@12-apps/ui 1.0.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components-catalog.md +14 -0
- package/package.json +42 -27
- package/src/__tests__/data-grid.test.tsx +125 -0
- package/src/__tests__/package-wiring.test.ts +12 -8
- package/src/__tests__/social-login-button.test.tsx +5 -5
- package/src/__tests__/viewport.test.ts +50 -0
- package/src/charts/index.ts +8 -0
- package/src/components/data-display/Alert/Alert.md +1 -1
- package/src/components/data-display/Alert/Alert.stories.tsx +5 -7
- package/src/components/data-display/Alert/Alert.styles.tsx +255 -0
- package/src/components/data-display/Alert/Alert.test.stories.tsx +11 -13
- package/src/components/data-display/Alert/Alert.tsx +135 -328
- package/src/components/data-display/AlertDialog/AlertDialog.stories.tsx +3 -1
- package/src/components/data-display/AlertDialog/AlertDialog.tsx +191 -78
- package/src/components/data-display/AlertDialog/AlertDialog.types.ts +8 -1
- package/src/components/data-display/AsyncStateContainer/AsyncStateContainer.test.stories.tsx +5 -5
- package/src/components/data-display/AsyncStateContainer/AsyncStateContainer.tsx +44 -35
- package/src/components/data-display/Avatar/Avatar.stories.tsx +4 -1
- package/src/components/data-display/Avatar/Avatar.test.stories.tsx +5 -3
- package/src/components/data-display/Avatar/Avatar.tsx +154 -450
- package/src/components/data-display/Avatar/Avatar.view.tsx +396 -0
- package/src/components/data-display/Badge/Badge.animations.ts +67 -0
- package/src/components/data-display/Badge/Badge.helpers.ts +74 -0
- package/src/components/data-display/Badge/Badge.stories.tsx +14 -16
- package/src/components/data-display/Badge/Badge.styles.ts +261 -0
- package/src/components/data-display/Badge/Badge.test.stories.tsx +32 -31
- package/src/components/data-display/Badge/Badge.tsx +151 -557
- package/src/components/data-display/Badge/Badge.variants.ts +106 -0
- package/src/components/data-display/Badge/BadgeContent.tsx +107 -0
- package/src/components/data-display/Banner/Banner.stories.tsx +5 -1
- package/src/components/data-display/Banner/Banner.styles.ts +120 -0
- package/src/components/data-display/Banner/Banner.tsx +179 -224
- package/src/components/data-display/Chart/Chart.md +48 -1
- package/src/components/data-display/Chart/Chart.tsx +82 -500
- package/src/components/data-display/Chart/Chart.types.ts +2 -0
- package/src/components/data-display/Chart/ChartComposables.tsx +121 -0
- package/src/components/data-display/Chart/ChartSpec.types.ts +70 -0
- package/src/components/data-display/Chart/SpecChart.stories.tsx +150 -0
- package/src/components/data-display/Chart/SpecChart.test.stories.tsx +56 -0
- package/src/components/data-display/Chart/SpecChart.tsx +53 -0
- package/src/components/data-display/Chart/__tests__/SpecChart.test.tsx +60 -0
- package/src/components/data-display/Chart/__tests__/chart-format.test.ts +43 -0
- package/src/components/data-display/Chart/__tests__/chart-spec.test.ts +119 -0
- package/src/components/data-display/Chart/chart-format.ts +26 -0
- package/src/components/data-display/Chart/chart-internals.ts +148 -0
- package/src/components/data-display/Chart/chart-renderers.tsx +338 -0
- package/src/components/data-display/Chart/chart-spec-registry.ts +74 -0
- package/src/components/data-display/Chart/chart-spec.ts +171 -0
- package/src/components/data-display/Chart/index.ts +30 -0
- package/src/components/data-display/Chip/Chip.helpers.tsx +131 -0
- package/src/components/data-display/Chip/Chip.stories.tsx +2 -1
- package/src/components/data-display/Chip/Chip.test.stories.tsx +11 -8
- package/src/components/data-display/Chip/Chip.tsx +31 -94
- package/src/components/data-display/DataGrid/DataGrid.body.tsx +141 -0
- package/src/components/data-display/DataGrid/DataGrid.cells.tsx +238 -0
- package/src/components/data-display/DataGrid/DataGrid.model.ts +315 -0
- package/src/components/data-display/DataGrid/DataGrid.rows.ts +112 -0
- package/src/components/data-display/DataGrid/DataGrid.states.tsx +102 -0
- package/src/components/data-display/DataGrid/DataGrid.test.stories.tsx +6 -12
- package/src/components/data-display/DataGrid/DataGrid.tsx +129 -725
- package/src/components/data-display/DataViews/DataViewsGrid.tsx +187 -0
- package/src/components/data-display/DataViews/DataViewsTableBase.tsx +400 -0
- package/src/components/data-display/DataViews/ManageViewsDialog.tsx +140 -0
- package/src/components/data-display/DataViews/SaveViewModal.tsx +289 -0
- package/src/components/data-display/DataViews/__tests__/DataViewsGrid.test.tsx +271 -0
- package/src/components/data-display/DataViews/__tests__/DataViewsTableBase.test.tsx +134 -0
- package/src/components/data-display/DataViews/__tests__/SaveViewModal.test.tsx +83 -0
- package/src/components/data-display/DataViews/__tests__/data-views-filter.test.ts +115 -0
- package/src/components/data-display/DataViews/__tests__/data-views-preview.test.ts +53 -0
- package/src/components/data-display/DataViews/__tests__/range-pill.test.tsx +114 -0
- package/src/components/data-display/DataViews/base-card.stories.tsx +136 -0
- package/src/components/data-display/DataViews/base-card.tsx +278 -0
- package/src/components/data-display/DataViews/data-views-filter-panel.tsx +184 -0
- package/src/components/data-display/DataViews/data-views-filter.ts +138 -0
- package/src/components/data-display/DataViews/data-views-grid-helpers.tsx +248 -0
- package/src/components/data-display/DataViews/data-views-grid-parts.tsx +400 -0
- package/src/components/data-display/DataViews/data-views-inline-bar.tsx +257 -0
- package/src/components/data-display/DataViews/data-views-layout-context.tsx +176 -0
- package/src/components/data-display/DataViews/data-views-pagination.tsx +31 -0
- package/src/components/data-display/DataViews/data-views-preview.ts +53 -0
- package/src/components/data-display/DataViews/data-views-range-pill.tsx +277 -0
- package/src/components/data-display/DataViews/data-views-table-parts.tsx +150 -0
- package/src/components/data-display/DataViews/data-views-toolbar.tsx +149 -0
- package/src/components/data-display/DataViews/data-views-types.ts +312 -0
- package/src/components/data-display/DataViews/data-views-view-mutations.ts +68 -0
- package/src/components/data-display/DataViews/index.ts +33 -0
- package/src/components/data-display/DataViews/use-data-views-state.ts +349 -0
- package/src/components/data-display/DateTimeDisplay/DateTimeDisplay.hooks.ts +104 -0
- package/src/components/data-display/DateTimeDisplay/DateTimeDisplay.tsx +68 -118
- package/src/components/data-display/DescriptionItem/DescriptionItem.stories.tsx +1 -1
- package/src/components/data-display/DescriptionItem/DescriptionItem.tsx +52 -34
- package/src/components/data-display/EmptyState/EmptyState.test.stories.tsx +6 -6
- package/src/components/data-display/EmptyState/EmptyState.tsx +218 -135
- package/src/components/data-display/ErrorState/ErrorState.test.stories.tsx +2 -2
- package/src/components/data-display/ErrorState/ErrorState.tsx +83 -55
- package/src/components/data-display/HoverCard/HoverCard.stories.tsx +3 -1
- package/src/components/data-display/HoverCard/HoverCard.test.stories.tsx +5 -5
- package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.hooks.ts +99 -0
- package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.styles.ts +87 -0
- package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.tsx +28 -155
- package/src/components/data-display/LazyImage/LazyImage.hooks.ts +203 -0
- package/src/components/data-display/LazyImage/LazyImage.md +1 -1
- package/src/components/data-display/LazyImage/LazyImage.stories.tsx +3 -3
- package/src/components/data-display/LazyImage/LazyImage.tsx +144 -243
- package/src/components/data-display/Lightbox/Lightbox.constants.ts +53 -0
- package/src/components/data-display/Lightbox/Lightbox.helpers.ts +39 -0
- package/src/components/data-display/Lightbox/Lightbox.hooks.ts +259 -0
- package/src/components/data-display/Lightbox/Lightbox.test.stories.tsx +28 -35
- package/src/components/data-display/Lightbox/Lightbox.tsx +98 -658
- package/src/components/data-display/Lightbox/LightboxControls.tsx +152 -0
- package/src/components/data-display/Lightbox/LightboxCurrentMedia.tsx +50 -0
- package/src/components/data-display/Lightbox/LightboxMedia.tsx +76 -0
- package/src/components/data-display/Lightbox/LightboxOverlay.tsx +83 -0
- package/src/components/data-display/Lightbox/LightboxStage.tsx +97 -0
- package/src/components/data-display/Lightbox/LightboxThumbnails.tsx +83 -0
- package/src/components/data-display/Lightbox/useLightbox.ts +80 -0
- package/src/components/data-display/LoadingState/LoadingState.helpers.ts +52 -0
- package/src/components/data-display/LoadingState/LoadingState.tsx +117 -87
- package/src/components/data-display/MapPreview/MapControlBar.tsx +116 -0
- package/src/components/data-display/MapPreview/MapDecorations.tsx +155 -0
- package/src/components/data-display/MapPreview/MapLayers.tsx +70 -0
- package/src/components/data-display/MapPreview/MapMarkers.tsx +84 -0
- package/src/components/data-display/MapPreview/MapOverlays.tsx +68 -0
- package/src/components/data-display/MapPreview/MapPreview.constants.ts +19 -0
- package/src/components/data-display/MapPreview/MapPreview.helpers.ts +63 -0
- package/src/components/data-display/MapPreview/MapPreview.hooks.ts +265 -0
- package/src/components/data-display/MapPreview/MapPreview.test.stories.tsx +14 -10
- package/src/components/data-display/MapPreview/MapPreview.tsx +52 -882
- package/src/components/data-display/MapPreview/MapPreview.types.ts +8 -1
- package/src/components/data-display/MapPreview/MapSearchBar.tsx +64 -0
- package/src/components/data-display/MapPreview/MapTiles.tsx +70 -0
- package/src/components/data-display/MapPreview/StaticMapView.tsx +63 -0
- package/src/components/data-display/MapPreview/mapProjection.ts +52 -0
- package/src/components/data-display/MapPreview/mapSurface.ts +37 -0
- package/src/components/data-display/MapPreview/useMapPreview.ts +62 -0
- package/src/components/data-display/Popover/Popover.test.stories.tsx +13 -19
- package/src/components/data-display/Progress/Progress.helpers.ts +37 -0
- package/src/components/data-display/Progress/Progress.styles.ts +101 -0
- package/src/components/data-display/Progress/Progress.tsx +185 -213
- package/src/components/data-display/SectionOnboarding/SectionOnboarding.stories.tsx +1 -1
- package/src/components/data-display/SectionOnboarding/SectionOnboarding.tsx +1 -1
- package/src/components/data-display/StatCard/StatCard.md +117 -0
- package/src/components/data-display/StatCard/StatCard.stories.tsx +167 -0
- package/src/components/data-display/StatCard/StatCard.test.stories.tsx +240 -0
- package/src/components/data-display/StatCard/StatCard.tsx +236 -0
- package/src/components/data-display/StatCard/StatCard.types.ts +62 -0
- package/src/components/data-display/StatCard/index.ts +7 -0
- package/src/components/data-display/Stepper/Stepper.test.stories.tsx +12 -19
- package/src/components/data-display/Stepper/Stepper.tsx +1 -1
- package/src/components/data-display/Table/Table.helpers.ts +31 -0
- package/src/components/data-display/Table/Table.hooks.ts +161 -0
- package/src/components/data-display/Table/Table.stories.tsx +3 -1
- package/src/components/data-display/Table/Table.styles.ts +223 -0
- package/src/components/data-display/Table/Table.test.stories.tsx +4 -4
- package/src/components/data-display/Table/Table.tsx +291 -748
- package/src/components/data-display/Table/TableParts.hooks.tsx +112 -0
- package/src/components/data-display/Table/TableParts.tsx +298 -0
- package/src/components/data-display/Table/TableStates.tsx +21 -0
- package/src/components/data-display/Timeline/Timeline.stories.tsx +10 -12
- package/src/components/data-display/Timeline/Timeline.test.stories.tsx +30 -44
- package/src/components/data-display/Timeline/Timeline.tsx +23 -284
- package/src/components/data-display/Timeline/TimelineRow.tsx +346 -0
- package/src/components/data-display/TimingDiagram/TimingDiagram.tsx +38 -311
- package/src/components/data-display/TimingDiagram/TimingDiagram.views.tsx +346 -0
- package/src/components/data-display/Tooltip/Tooltip.stories.tsx +4 -1
- package/src/components/data-display/Tooltip/Tooltip.test.stories.tsx +44 -58
- package/src/components/data-display/Tooltip/Tooltip.tsx +67 -62
- package/src/components/data-display/index.ts +1 -0
- package/src/components/feedback/ConfirmAction/ConfirmAction.hooks.ts +96 -0
- package/src/components/feedback/ConfirmAction/ConfirmAction.md +140 -0
- package/src/components/feedback/ConfirmAction/ConfirmAction.parts.tsx +154 -0
- package/src/components/feedback/ConfirmAction/ConfirmAction.stories.tsx +146 -0
- package/src/components/feedback/ConfirmAction/ConfirmAction.test.stories.tsx +114 -0
- package/src/components/feedback/ConfirmAction/ConfirmAction.tsx +46 -0
- package/src/components/feedback/ConfirmAction/ConfirmAction.types.ts +111 -0
- package/src/components/feedback/ConfirmAction/ConfirmButton.tsx +18 -0
- package/src/components/feedback/ConfirmAction/__tests__/confirm-action.test.tsx +288 -0
- package/src/components/feedback/ConfirmAction/index.ts +16 -0
- package/src/components/feedback/ConfirmAction/with-confirmation.tsx +112 -0
- package/src/components/feedback/Dialog/Dialog.styles.ts +4 -2
- package/src/components/feedback/Dialog/Dialog.tsx +26 -1
- package/src/components/feedback/Dialog/__tests__/dialog-content-spacing.test.tsx +66 -0
- package/src/components/feedback/Modal/Modal.md +1 -1
- package/src/components/feedback/Modal/Modal.stories.tsx +2 -2
- package/src/components/feedback/Sonner/Sonner.tsx +104 -288
- package/src/components/feedback/Sonner/Sonner.types.ts +7 -0
- package/src/components/feedback/Sonner/SonnerToast.tsx +216 -0
- package/src/components/feedback/StackedModal/StackedModal.hooks.ts +226 -0
- package/src/components/feedback/StackedModal/StackedModal.parts.tsx +311 -0
- package/src/components/feedback/StackedModal/StackedModal.stories.tsx +13 -15
- package/src/components/feedback/StackedModal/StackedModal.styles.ts +193 -0
- package/src/components/feedback/StackedModal/StackedModal.tsx +100 -619
- package/src/components/feedback/StackedModal/StackedModal.types.ts +17 -3
- package/src/components/feedback/StackedModal/__tests__/stacked-modal-viewport.test.tsx +104 -0
- package/src/components/feedback/StackedModal/modal-stack.tsx +119 -0
- package/src/components/feedback/Toast/Toast.tsx +92 -81
- package/src/components/feedback/TutorialOverlay/TutorialOverlay.geometry.ts +65 -0
- package/src/components/feedback/TutorialOverlay/TutorialOverlay.hooks.ts +185 -0
- package/src/components/feedback/TutorialOverlay/TutorialOverlay.test.stories.tsx +2 -2
- package/src/components/feedback/TutorialOverlay/TutorialOverlay.tsx +156 -414
- package/src/components/feedback/TutorialOverlay/TutorialStepBody.tsx +255 -0
- package/src/components/feedback/index.ts +1 -0
- package/src/components/form/AddressAutocomplete/AddressAutocomplete.helpers.ts +58 -0
- package/src/components/form/AddressAutocomplete/AddressAutocomplete.hooks.ts +93 -0
- package/src/components/form/AddressAutocomplete/AddressAutocomplete.mapper.ts +57 -0
- package/src/components/form/AddressAutocomplete/AddressAutocomplete.stories.tsx +4 -2
- package/src/components/form/AddressAutocomplete/AddressAutocomplete.test.stories.tsx +20 -16
- package/src/components/form/AddressAutocomplete/AddressAutocomplete.tsx +26 -829
- package/src/components/form/AddressAutocomplete/AddressAutocomplete.types.ts +11 -0
- package/src/components/form/AddressAutocomplete/AddressAutocompleteField.tsx +86 -0
- package/src/components/form/AddressAutocomplete/AddressFallbackField.tsx +64 -0
- package/src/components/form/AddressAutocomplete/AddressInputField.tsx +80 -0
- package/src/components/form/AddressAutocomplete/AddressOption.tsx +83 -0
- package/src/components/form/AddressAutocomplete/AddressSuggestionsPaper.tsx +24 -0
- package/src/components/form/AddressAutocomplete/AddressTextField.tsx +43 -0
- package/src/components/form/AddressAutocomplete/mockPlaces.ts +208 -0
- package/src/components/form/AddressAutocomplete/useAddressPredictions.ts +136 -0
- package/src/components/form/AddressAutocomplete/useAddressSelection.ts +113 -0
- package/src/components/form/AddressAutocomplete/useCurrentLocation.ts +116 -0
- package/src/components/form/AddressAutocomplete/useGoogleMapsAPI.ts +75 -0
- package/src/components/form/Button/Button.helpers.ts +36 -0
- package/src/components/form/Button/Button.stories.tsx +4 -1
- package/src/components/form/Button/Button.styles.ts +225 -0
- package/src/components/form/Button/Button.test.stories.tsx +25 -21
- package/src/components/form/Button/Button.tsx +45 -252
- package/src/components/form/Calendar/Calendar.dates.ts +100 -0
- package/src/components/form/Calendar/Calendar.helpers.ts +32 -0
- package/src/components/form/Calendar/Calendar.hooks.ts +278 -0
- package/src/components/form/Calendar/Calendar.keyboard.ts +32 -0
- package/src/components/form/Calendar/Calendar.selection.ts +156 -0
- package/src/components/form/Calendar/Calendar.test.stories.tsx +33 -34
- package/src/components/form/Calendar/Calendar.tsx +63 -551
- package/src/components/form/Calendar/Calendar.types.ts +16 -0
- package/src/components/form/Calendar/CalendarDay.tsx +127 -0
- package/src/components/form/Calendar/CalendarDayCell.tsx +52 -0
- package/src/components/form/Calendar/CalendarMonth.tsx +128 -0
- package/src/components/form/CepField/CepField.md +138 -0
- package/src/components/form/CepField/CepField.stories.tsx +138 -0
- package/src/components/form/CepField/CepField.test.stories.tsx +281 -0
- package/src/components/form/CepField/CepField.tsx +211 -0
- package/src/components/form/CepField/CepField.types.ts +73 -0
- package/src/components/form/CepField/index.ts +2 -0
- package/src/components/form/Checkbox/Checkbox.helpers.ts +28 -0
- package/src/components/form/Checkbox/Checkbox.tsx +19 -16
- package/src/components/form/CodeEditor/CodeEditor.helpers.ts +45 -0
- package/src/components/form/CodeEditor/CodeEditor.hooks.ts +104 -0
- package/src/components/form/CodeEditor/CodeEditor.monaco.ts +109 -0
- package/src/components/form/CodeEditor/CodeEditor.test.stories.tsx +17 -18
- package/src/components/form/CodeEditor/CodeEditor.tsx +81 -330
- package/src/components/form/CodeEditor/CodeEditorToolbar.tsx +168 -0
- package/src/components/form/Command/Command.hooks.ts +207 -0
- package/src/components/form/Command/Command.parts.tsx +58 -0
- package/src/components/form/Command/Command.styles.ts +83 -0
- package/src/components/form/Command/Command.tsx +61 -270
- package/src/components/form/CreatableSelect/CreatableSelect.md +1 -1
- package/src/components/form/CreatableSelect/CreatableSelect.test.stories.tsx +1 -1
- package/src/components/form/CreatableSelect/CreatableSelect.tsx +152 -71
- package/src/components/form/CreatableSelect/CreatableSelect.types.ts +13 -0
- package/src/components/form/HeaderButton/HeaderButton.tsx +80 -0
- package/src/components/form/HeaderButton/index.ts +2 -0
- package/src/components/form/Input/Input.stories.tsx +5 -1
- package/src/components/form/Input/Input.styles.ts +176 -0
- package/src/components/form/Input/Input.test.stories.tsx +30 -23
- package/src/components/form/Input/Input.tsx +71 -211
- package/src/components/form/Input/Input.types.ts +11 -0
- package/src/components/form/InputOTP/InputOTP.test.stories.tsx +9 -17
- package/src/components/form/Menubar/Menubar.test.stories.tsx +4 -8
- package/src/components/form/PasswordStrength/PasswordStrength.helpers.tsx +117 -0
- package/src/components/form/PasswordStrength/PasswordStrength.test.stories.tsx +3 -3
- package/src/components/form/PasswordStrength/PasswordStrength.tsx +202 -255
- package/src/components/form/PhoneInput/PhoneInput.hooks.ts +134 -0
- package/src/components/form/PhoneInput/PhoneInput.test.stories.tsx +4 -4
- package/src/components/form/PhoneInput/PhoneInput.tsx +279 -292
- package/src/components/form/PhoneInput/countries.ts +54 -0
- package/src/components/form/PhoneInput/phone.ts +47 -0
- package/src/components/form/RadioGroup/RadioGroup.test.stories.tsx +9 -9
- package/src/components/form/RichTextEditor/RichTextEditor.dom.ts +124 -0
- package/src/components/form/RichTextEditor/RichTextEditor.format.ts +61 -0
- package/src/components/form/RichTextEditor/RichTextEditor.hooks.ts +142 -0
- package/src/components/form/RichTextEditor/RichTextEditor.test.stories.tsx +5 -12
- package/src/components/form/RichTextEditor/RichTextEditor.tsx +98 -410
- package/src/components/form/RichTextEditor/RichTextEditorToolbar.tsx +187 -0
- package/src/components/form/SearchPalette/SearchPalette.parts.tsx +22 -0
- package/src/components/form/SearchPalette/SearchPalette.tsx +13 -9
- package/src/components/form/Select/Select.test.stories.tsx +35 -27
- package/src/components/form/Slider/Slider.test.stories.tsx +9 -11
- package/src/components/form/Switch/Switch.md +2 -1
- package/src/components/form/Switch/Switch.test.stories.tsx +6 -6
- package/src/components/form/Textarea/Textarea.styles.ts +143 -0
- package/src/components/form/Textarea/Textarea.test.stories.tsx +20 -26
- package/src/components/form/Textarea/Textarea.tsx +114 -535
- package/src/components/form/Textarea/TextareaRichEditor.tsx +377 -0
- package/src/components/form/Toggle/Toggle.styles.ts +211 -0
- package/src/components/form/Toggle/Toggle.test.stories.tsx +19 -27
- package/src/components/form/Toggle/Toggle.tsx +10 -185
- package/src/components/form/UploadButton/UploadButton.hooks.ts +323 -0
- package/src/components/form/UploadButton/UploadButton.parts.tsx +210 -0
- package/src/components/form/UploadButton/UploadButton.stories.tsx +2 -1
- package/src/components/form/UploadButton/UploadButton.test.stories.tsx +123 -4
- package/src/components/form/UploadButton/UploadButton.tsx +76 -305
- package/src/components/form/UploadButton/UploadButton.types.ts +16 -0
- package/src/components/form/index.ts +1 -0
- package/src/components/form/total-form/Fields/CepField.tsx +143 -0
- package/src/components/form/total-form/Fields/ToggleField.tsx +61 -0
- package/src/components/form/total-form/Fields/index.ts +4 -0
- package/src/components/form/total-form/FormContainer.tsx +3 -3
- package/src/components/form/total-form/SubmitButton.tsx +25 -2
- package/src/components/form/total-form/total-form.test.tsx +118 -1
- package/src/components/layout/Accordion/Accordion.md +1 -1
- package/src/components/layout/Accordion/Accordion.stories.tsx +5 -1
- package/src/components/layout/Accordion/Accordion.styles.ts +80 -0
- package/src/components/layout/Accordion/Accordion.test.stories.tsx +32 -50
- package/src/components/layout/Accordion/Accordion.tsx +2 -75
- package/src/components/layout/Card/Card.helpers.ts +30 -0
- package/src/components/layout/Card/Card.stories.tsx +9 -11
- package/src/components/layout/Card/Card.styles.ts +182 -0
- package/src/components/layout/Card/Card.test.stories.tsx +46 -46
- package/src/components/layout/Card/Card.tsx +24 -241
- package/src/components/layout/CardGrid/CardGrid.md +96 -0
- package/src/components/layout/CardGrid/CardGrid.tsx +140 -0
- package/src/components/layout/CardGrid/__tests__/CardGrid.test.tsx +92 -0
- package/src/components/layout/CardGrid/index.ts +2 -0
- package/src/components/layout/Collapsible/Collapsible.helpers.ts +83 -0
- package/src/components/layout/Collapsible/Collapsible.test.stories.tsx +7 -3
- package/src/components/layout/Collapsible/Collapsible.tsx +111 -107
- package/src/components/layout/Container/Container.test.stories.tsx +11 -16
- package/src/components/layout/ContentToolbar/ColumnsMenu.tsx +1 -1
- package/src/components/layout/ContentToolbar/ContentToolbar.tsx +120 -44
- package/src/components/layout/ContentToolbar/ContentToolbar.types.ts +9 -0
- package/src/components/layout/ContentToolbar/FilterTrigger.tsx +1 -1
- package/src/components/layout/ContentToolbar/MultiSelectDropdown.tsx +58 -43
- package/src/components/layout/ContentToolbar/MultiSelectTriggers.tsx +139 -0
- package/src/components/layout/ContentToolbar/SavedViewsMenu.tsx +10 -3
- package/src/components/layout/ContentToolbar/SortByDropdown.tsx +35 -11
- package/src/components/layout/ContentToolbar/ViewSelector.tsx +5 -6
- package/src/components/layout/ContentToolbar/__tests__/MultiSelectDropdown.test.tsx +21 -0
- package/src/components/layout/Dashboard/Dashboard.test.stories.tsx +27 -1
- package/src/components/layout/Dashboard/Dashboard.tsx +27 -8
- package/src/components/layout/Dashboard/DashboardParts.tsx +12 -19
- package/src/components/layout/Dashboard/index.ts +2 -1
- package/src/components/layout/Drawer/Drawer.stories.tsx +8 -10
- package/src/components/layout/Drawer/Drawer.test.stories.tsx +18 -14
- package/src/components/layout/Drawer/Drawer.tsx +67 -53
- package/src/components/layout/Resizable/Resizable.helpers.ts +133 -0
- package/src/components/layout/Resizable/Resizable.hooks.ts +68 -0
- package/src/components/layout/Resizable/Resizable.tsx +38 -153
- package/src/components/layout/ScrollArea/ScrollArea.helpers.ts +51 -0
- package/src/components/layout/ScrollArea/ScrollArea.styles.ts +248 -0
- package/src/components/layout/ScrollArea/ScrollArea.tsx +183 -374
- package/src/components/layout/ScrollArea/ScrollAreaContent.tsx +70 -0
- package/src/components/layout/ScrollArea/ScrollViewport.tsx +90 -0
- package/src/components/layout/Separator/Separator.styles.ts +128 -0
- package/src/components/layout/Separator/Separator.test.stories.tsx +3 -12
- package/src/components/layout/Separator/Separator.tsx +6 -132
- package/src/components/layout/SettingsLayout/SettingsLayout.parts.tsx +2 -1
- package/src/components/layout/SettingsLayout/SettingsLayout.stories.tsx +6 -8
- package/src/components/layout/Sidebar/Sidebar.stories.tsx +10 -12
- package/src/components/layout/Sidebar/Sidebar.test.stories.tsx +35 -37
- package/src/components/layout/Skeleton/Skeleton.helpers.ts +33 -0
- package/src/components/layout/Skeleton/Skeleton.styles.ts +114 -0
- package/src/components/layout/Skeleton/Skeleton.test.stories.tsx +3 -3
- package/src/components/layout/Skeleton/Skeleton.tsx +67 -155
- package/src/components/layout/TableFilter/TableFilter.tsx +2 -1
- package/src/components/navigation/Breadcrumbs/BreadcrumbEntry.tsx +305 -0
- package/src/components/navigation/Breadcrumbs/Breadcrumbs.md +2 -1
- package/src/components/navigation/Breadcrumbs/Breadcrumbs.stories.tsx +12 -14
- package/src/components/navigation/Breadcrumbs/Breadcrumbs.styles.tsx +212 -0
- package/src/components/navigation/Breadcrumbs/Breadcrumbs.test.stories.tsx +62 -61
- package/src/components/navigation/Breadcrumbs/Breadcrumbs.tsx +116 -429
- package/src/components/navigation/CommandPalette/CommandPalette.helpers.ts +82 -0
- package/src/components/navigation/CommandPalette/CommandPalette.hooks.ts +202 -0
- package/src/components/navigation/CommandPalette/CommandPalette.stories.tsx +3 -1
- package/src/components/navigation/CommandPalette/CommandPalette.test.stories.tsx +3 -5
- package/src/components/navigation/CommandPalette/CommandPalette.tsx +108 -341
- package/src/components/navigation/CommandPalette/CommandPaletteResults.tsx +182 -0
- package/src/components/navigation/CommandPalette/ShortcutChip.tsx +18 -0
- package/src/components/navigation/ContextMenu/ContextMenu.md +3 -1
- package/src/components/navigation/ContextMenu/ContextMenu.stories.tsx +4 -1
- package/src/components/navigation/ContextMenu/ContextMenu.test.stories.tsx +19 -24
- package/src/components/navigation/ContextMenu/ContextMenu.tsx +70 -54
- package/src/components/navigation/DropdownMenu/DropdownMenu.md +3 -1
- package/src/components/navigation/DropdownMenu/DropdownMenu.stories.tsx +17 -19
- package/src/components/navigation/DropdownMenu/DropdownMenu.test.stories.tsx +26 -26
- package/src/components/navigation/DropdownMenu/DropdownMenu.tsx +189 -111
- package/src/components/navigation/NavigationMenu/NavigationMegaSection.tsx +66 -0
- package/src/components/navigation/NavigationMenu/NavigationMenu.md +7 -2
- package/src/components/navigation/NavigationMenu/NavigationMenu.shell.tsx +218 -0
- package/src/components/navigation/NavigationMenu/NavigationMenu.styles.ts +223 -0
- package/src/components/navigation/NavigationMenu/NavigationMenu.tsx +119 -808
- package/src/components/navigation/NavigationMenu/NavigationMenuItem.hooks.ts +126 -0
- package/src/components/navigation/NavigationMenu/NavigationMenuItem.tsx +352 -0
- package/src/components/navigation/NavigationMenu/NavigationStandardBody.tsx +148 -0
- package/src/components/navigation/NavigationMenu/stories/NavigationMenu.stories.tsx +15 -17
- package/src/components/navigation/NavigationMenu/stories/NavigationMenu.test.stories.tsx +27 -23
- package/src/components/navigation/NavigationMenu/stories/NavigationMenuHorizontal.stories.tsx +11 -13
- package/src/components/navigation/NavigationMenu/stories/NavigationMenuVertical.stories.tsx +9 -11
- package/src/components/navigation/Pagination/Pagination.helpers.tsx +85 -0
- package/src/components/navigation/Pagination/Pagination.md +2 -1
- package/src/components/navigation/Pagination/Pagination.stories.tsx +4 -6
- package/src/components/navigation/Pagination/Pagination.styles.ts +135 -0
- package/src/components/navigation/Pagination/Pagination.tsx +178 -313
- package/src/components/navigation/Pagination/PaginationParts.tsx +81 -0
- package/src/components/navigation/Tabs/Tabs.md +3 -1
- package/src/components/navigation/Tabs/Tabs.stories.tsx +21 -23
- package/src/components/navigation/Tabs/Tabs.styles.ts +163 -0
- package/src/components/navigation/Tabs/Tabs.test.stories.tsx +37 -37
- package/src/components/navigation/Tabs/Tabs.tsx +192 -361
- package/src/components/navigation/Tabs/TabsPanel.tsx +100 -0
- package/src/components/typography/Blockquote/Blockquote.tsx +1 -1
- package/src/components/typography/Code/Code.test.stories.tsx +5 -5
- package/src/components/typography/Code/Code.tsx +74 -49
- package/src/components/typography/Heading/Heading.test.stories.tsx +8 -8
- package/src/components/typography/Paragraph/Paragraph.test.stories.tsx +5 -12
- package/src/components/typography/Text/Text.tsx +84 -77
- package/src/components/utility/AnimatedIcon/AnimatedIcon.md +4 -4
- package/src/components/utility/AnimatedIcon/AnimatedIcon.stories.tsx +19 -21
- package/src/components/utility/AnimatedIcon/AnimatedIcon.test.stories.tsx +19 -12
- package/src/components/utility/AspectRatio/AspectRatio.test.stories.tsx +9 -9
- package/src/components/utility/InfiniteScroll/InfiniteScroll.hooks.ts +118 -0
- package/src/components/utility/InfiniteScroll/InfiniteScroll.parts.tsx +54 -0
- package/src/components/utility/InfiniteScroll/InfiniteScroll.test.stories.tsx +12 -9
- package/src/components/utility/InfiniteScroll/InfiniteScroll.tsx +29 -148
- package/src/components/utility/InstallPrompt/InstallPrompt.helpers.ts +124 -0
- package/src/components/utility/InstallPrompt/InstallPrompt.md +113 -0
- package/src/components/utility/InstallPrompt/InstallPrompt.stories.tsx +118 -0
- package/src/components/utility/InstallPrompt/InstallPrompt.styles.ts +50 -0
- package/src/components/utility/InstallPrompt/InstallPrompt.test.stories.tsx +83 -0
- package/src/components/utility/InstallPrompt/InstallPrompt.tsx +192 -0
- package/src/components/utility/InstallPrompt/InstallPrompt.types.ts +78 -0
- package/src/components/utility/InstallPrompt/__tests__/InstallPrompt.ios.test.tsx +65 -0
- package/src/components/utility/InstallPrompt/__tests__/usePwaInstall.test.tsx +271 -0
- package/src/components/utility/InstallPrompt/index.ts +10 -0
- package/src/components/utility/InstallPrompt/usePwaInstall.ts +171 -0
- package/src/components/utility/Transition/Transition.helpers.ts +40 -0
- package/src/components/utility/Transition/Transition.tsx +17 -68
- package/src/components/utility/VirtualList/VirtualList.hooks.ts +319 -0
- package/src/components/utility/VirtualList/VirtualList.test.stories.tsx +49 -38
- package/src/components/utility/VirtualList/VirtualList.tsx +110 -315
- package/src/components/utility/WorkflowStep/WorkflowStep.helpers.ts +151 -0
- package/src/components/utility/WorkflowStep/WorkflowStep.parts.tsx +371 -0
- package/src/components/utility/WorkflowStep/WorkflowStep.stories.tsx +9 -11
- package/src/components/utility/WorkflowStep/WorkflowStep.test.stories.tsx +7 -7
- package/src/components/utility/WorkflowStep/WorkflowStep.tsx +51 -449
- package/src/components/utility/WorkflowStep/WorkflowStepRow.tsx +96 -0
- package/src/components/utility/index.ts +1 -0
- package/src/mui/Link.ts +2 -0
- package/src/utils/index.ts +2 -1
- package/src/utils/viewport.ts +65 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `InstallPrompt` on iOS Safari.
|
|
3
|
+
*
|
|
4
|
+
* Its own file because the platform probe has to be replaced at module scope,
|
|
5
|
+
* and `vi.mock` is hoisted over the whole file — the Chromium cases in
|
|
6
|
+
* `usePwaInstall.test.tsx` must keep the real probe.
|
|
7
|
+
*
|
|
8
|
+
* What matters here is the ABSENCE of the install button. Safari exposes no way
|
|
9
|
+
* to open its own Add to Home Screen sheet, so a button on iOS would be a
|
|
10
|
+
* control that does nothing when tapped; the instructions take its place.
|
|
11
|
+
*/
|
|
12
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
13
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
14
|
+
|
|
15
|
+
import { InstallPrompt } from '../index';
|
|
16
|
+
|
|
17
|
+
vi.mock('../InstallPrompt.helpers', async (importOriginal) => {
|
|
18
|
+
const actual = await importOriginal<typeof import('../InstallPrompt.helpers')>();
|
|
19
|
+
|
|
20
|
+
return { ...actual, resolveInstallPlatform: () => 'ios' as const };
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('InstallPrompt on iOS Safari', () => {
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
window.localStorage.clear();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('shows without waiting for an event that iOS never fires', async () => {
|
|
29
|
+
render(<InstallPrompt title="Instalar FutureDrink" />);
|
|
30
|
+
|
|
31
|
+
await waitFor(() => {
|
|
32
|
+
expect(screen.getByTestId('install-prompt')).toBeInTheDocument();
|
|
33
|
+
});
|
|
34
|
+
expect(screen.getByTestId('install-prompt-title')).toHaveTextContent('Instalar FutureDrink');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('offers instructions instead of an install button', async () => {
|
|
38
|
+
render(<InstallPrompt />);
|
|
39
|
+
|
|
40
|
+
await waitFor(() => {
|
|
41
|
+
expect(screen.getByTestId('install-prompt-ios-instructions')).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
await waitFor(() => {
|
|
44
|
+
expect(screen.queryByTestId('install-prompt-install')).not.toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('accepts host-supplied instruction copy', async () => {
|
|
49
|
+
render(<InstallPrompt iosInstructions={<span>Toque em Compartilhar</span>} />);
|
|
50
|
+
|
|
51
|
+
await waitFor(() => {
|
|
52
|
+
expect(screen.getByTestId('install-prompt-ios-instructions')).toHaveTextContent(
|
|
53
|
+
'Toque em Compartilhar',
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('can still be dismissed', async () => {
|
|
59
|
+
render(<InstallPrompt />);
|
|
60
|
+
|
|
61
|
+
await waitFor(() => {
|
|
62
|
+
expect(screen.getByTestId('install-prompt-dismiss')).toBeInTheDocument();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `usePwaInstall` / `InstallPrompt` — the machinery that turns an installable
|
|
3
|
+
* site into one that actually asks.
|
|
4
|
+
*
|
|
5
|
+
* The regression this file exists for: a manifest and a service worker satisfy
|
|
6
|
+
* every installability criterion a browser checks, and still produce no prompt
|
|
7
|
+
* of any kind, because nothing in the page captures `beforeinstallprompt`. The
|
|
8
|
+
* `preventDefault` case below guards that hole — without that call the browser
|
|
9
|
+
* reclaims the event and the app can never install itself.
|
|
10
|
+
*
|
|
11
|
+
* The iOS branch lives in `InstallPrompt.ios.test.tsx`, which needs a
|
|
12
|
+
* module-level mock this file must not carry.
|
|
13
|
+
*/
|
|
14
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
15
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
16
|
+
|
|
17
|
+
import { InstallPrompt } from '../index';
|
|
18
|
+
import {
|
|
19
|
+
isDismissalActive,
|
|
20
|
+
isIosSafariAgent,
|
|
21
|
+
readDismissedAt,
|
|
22
|
+
writeDismissedAt,
|
|
23
|
+
} from '../InstallPrompt.helpers';
|
|
24
|
+
import type { BeforeInstallPromptEvent } from '../InstallPrompt.types';
|
|
25
|
+
import { usePwaInstall } from '../usePwaInstall';
|
|
26
|
+
|
|
27
|
+
const STORAGE_KEY = 'pwa-install-dismissed';
|
|
28
|
+
|
|
29
|
+
// A fixed clock. Deriving expectations from the real `Date.now()` would make
|
|
30
|
+
// them depend on how long the test took to run.
|
|
31
|
+
const NOW = 1_700_000_000_000;
|
|
32
|
+
const DAY = 86_400_000;
|
|
33
|
+
|
|
34
|
+
type MockPromptEvent = BeforeInstallPromptEvent & { prompt: ReturnType<typeof vi.fn> };
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Stands in for the Chromium event: `prompt()` records that it was called and
|
|
38
|
+
* `userChoice` resolves with whatever answer the test wants the user to give.
|
|
39
|
+
*/
|
|
40
|
+
const createPromptEvent = (outcome: 'accepted' | 'dismissed'): MockPromptEvent => {
|
|
41
|
+
const event = new Event('beforeinstallprompt') as MockPromptEvent;
|
|
42
|
+
|
|
43
|
+
Object.assign(event, {
|
|
44
|
+
platforms: ['web'],
|
|
45
|
+
prompt: vi.fn().mockResolvedValue(undefined),
|
|
46
|
+
userChoice: Promise.resolve({ outcome, platform: 'web' }),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return event;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Renders the hook's state into the DOM so assertions can read it directly. */
|
|
53
|
+
const HookProbe = ({ dismissForDays }: { dismissForDays?: number }) => {
|
|
54
|
+
const { canInstall, platform, isInstalled } = usePwaInstall({ dismissForDays });
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div>
|
|
58
|
+
<span data-testid="can-install">{String(canInstall)}</span>
|
|
59
|
+
<span data-testid="platform">{platform}</span>
|
|
60
|
+
<span data-testid="is-installed">{String(isInstalled)}</span>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const IPHONE_SAFARI =
|
|
66
|
+
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1';
|
|
67
|
+
const IPADOS_SAFARI =
|
|
68
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15';
|
|
69
|
+
const CHROME_ON_IOS =
|
|
70
|
+
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/120.0 Mobile/15E148 Safari/604.1';
|
|
71
|
+
const DESKTOP_CHROME =
|
|
72
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
|
73
|
+
|
|
74
|
+
describe('isIosSafariAgent', () => {
|
|
75
|
+
it('detects Safari on iPhone', () => {
|
|
76
|
+
expect(isIosSafariAgent(IPHONE_SAFARI, 5)).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('detects iPadOS, which reports a Macintosh agent with touch points', () => {
|
|
80
|
+
expect(isIosSafariAgent(IPADOS_SAFARI, 5)).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('rejects desktop Safari, which shares that agent but has no touch points', () => {
|
|
84
|
+
expect(isIosSafariAgent(IPADOS_SAFARI, 0)).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('rejects Chrome on iOS, which cannot add to the home screen at all', () => {
|
|
88
|
+
expect(isIosSafariAgent(CHROME_ON_IOS, 5)).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('rejects desktop Chrome', () => {
|
|
92
|
+
expect(isIosSafariAgent(DESKTOP_CHROME, 0)).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('InstallPrompt dismissal storage', () => {
|
|
97
|
+
afterEach(() => {
|
|
98
|
+
window.localStorage.clear();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('reports no dismissal when nothing was ever stored', () => {
|
|
102
|
+
expect(readDismissedAt(STORAGE_KEY)).toBeNull();
|
|
103
|
+
expect(isDismissalActive(null, 30, NOW)).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('round-trips a dismissal timestamp', () => {
|
|
107
|
+
writeDismissedAt(STORAGE_KEY, NOW);
|
|
108
|
+
expect(readDismissedAt(STORAGE_KEY)).toBe(NOW);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('ignores a corrupted stored value instead of throwing', () => {
|
|
112
|
+
window.localStorage.setItem(STORAGE_KEY, 'not-a-number');
|
|
113
|
+
expect(readDismissedAt(STORAGE_KEY)).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('keeps a dismissal active inside the window and expires it after', () => {
|
|
117
|
+
expect(isDismissalActive(NOW, 30, NOW + 29 * DAY)).toBe(true);
|
|
118
|
+
expect(isDismissalActive(NOW, 30, NOW + 31 * DAY)).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('treats dismissForDays of 0 as permanent', () => {
|
|
122
|
+
expect(isDismissalActive(NOW, 0, NOW + 3650 * DAY)).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('treats a future timestamp as active so a wound-back clock cannot re-prompt', () => {
|
|
126
|
+
expect(isDismissalActive(NOW + DAY, 30, NOW)).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('usePwaInstall', () => {
|
|
131
|
+
afterEach(() => {
|
|
132
|
+
window.localStorage.clear();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('cannot install before the browser offers a prompt', async () => {
|
|
136
|
+
render(<HookProbe />);
|
|
137
|
+
|
|
138
|
+
await waitFor(() => {
|
|
139
|
+
expect(screen.getByTestId('platform')).toHaveTextContent('unsupported');
|
|
140
|
+
});
|
|
141
|
+
expect(screen.getByTestId('can-install')).toHaveTextContent('false');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('becomes installable once beforeinstallprompt fires', async () => {
|
|
145
|
+
render(<HookProbe />);
|
|
146
|
+
fireEvent(window, createPromptEvent('accepted'));
|
|
147
|
+
|
|
148
|
+
await waitFor(() => {
|
|
149
|
+
expect(screen.getByTestId('can-install')).toHaveTextContent('true');
|
|
150
|
+
});
|
|
151
|
+
expect(screen.getByTestId('platform')).toHaveTextContent('prompt');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('calls preventDefault so the deferred handle survives', async () => {
|
|
155
|
+
render(<HookProbe />);
|
|
156
|
+
|
|
157
|
+
const event = createPromptEvent('accepted');
|
|
158
|
+
const preventDefault = vi.spyOn(event, 'preventDefault');
|
|
159
|
+
fireEvent(window, event);
|
|
160
|
+
|
|
161
|
+
await waitFor(() => {
|
|
162
|
+
expect(preventDefault).toHaveBeenCalled();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// `dismissForDays: 0` makes the stored dismissal permanent, so this asserts
|
|
167
|
+
// that the hook consults storage at all without depending on the wall clock.
|
|
168
|
+
// The expiry arithmetic itself is covered above against a fixed clock.
|
|
169
|
+
it('stays suppressed while a dismissal is stored', async () => {
|
|
170
|
+
writeDismissedAt(STORAGE_KEY, NOW);
|
|
171
|
+
|
|
172
|
+
render(<HookProbe dismissForDays={0} />);
|
|
173
|
+
fireEvent(window, createPromptEvent('accepted'));
|
|
174
|
+
|
|
175
|
+
await waitFor(() => {
|
|
176
|
+
expect(screen.getByTestId('platform')).toHaveTextContent('prompt');
|
|
177
|
+
});
|
|
178
|
+
expect(screen.getByTestId('can-install')).toHaveTextContent('false');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('reports installed and stays suppressed after appinstalled', async () => {
|
|
182
|
+
render(<HookProbe />);
|
|
183
|
+
fireEvent(window, createPromptEvent('accepted'));
|
|
184
|
+
|
|
185
|
+
await waitFor(() => {
|
|
186
|
+
expect(screen.getByTestId('can-install')).toHaveTextContent('true');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
fireEvent(window, new Event('appinstalled'));
|
|
190
|
+
|
|
191
|
+
await waitFor(() => {
|
|
192
|
+
expect(screen.getByTestId('is-installed')).toHaveTextContent('true');
|
|
193
|
+
});
|
|
194
|
+
expect(screen.getByTestId('can-install')).toHaveTextContent('false');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('InstallPrompt', () => {
|
|
199
|
+
afterEach(() => {
|
|
200
|
+
window.localStorage.clear();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('renders nothing until the browser offers a prompt', async () => {
|
|
204
|
+
render(<InstallPrompt />);
|
|
205
|
+
|
|
206
|
+
await waitFor(() => {
|
|
207
|
+
expect(screen.queryByTestId('install-prompt')).not.toBeInTheDocument();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('appears with an install button once the prompt is captured', async () => {
|
|
212
|
+
render(<InstallPrompt title="Install FutureDrink" />);
|
|
213
|
+
fireEvent(window, createPromptEvent('accepted'));
|
|
214
|
+
|
|
215
|
+
await waitFor(() => {
|
|
216
|
+
expect(screen.getByTestId('install-prompt')).toBeInTheDocument();
|
|
217
|
+
});
|
|
218
|
+
expect(screen.getByTestId('install-prompt-install')).toBeInTheDocument();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('fires the native prompt and reports the accepted outcome', async () => {
|
|
222
|
+
const onInstall = vi.fn();
|
|
223
|
+
|
|
224
|
+
render(<InstallPrompt onInstall={onInstall} />);
|
|
225
|
+
const event = createPromptEvent('accepted');
|
|
226
|
+
fireEvent(window, event);
|
|
227
|
+
|
|
228
|
+
await waitFor(() => {
|
|
229
|
+
expect(screen.getByTestId('install-prompt-install')).toBeInTheDocument();
|
|
230
|
+
});
|
|
231
|
+
fireEvent.click(screen.getByTestId('install-prompt-install'));
|
|
232
|
+
|
|
233
|
+
await waitFor(() => {
|
|
234
|
+
expect(onInstall).toHaveBeenCalledWith('accepted');
|
|
235
|
+
});
|
|
236
|
+
expect(event.prompt).toHaveBeenCalledTimes(1);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('records a dismissal when the user declines the native dialog', async () => {
|
|
240
|
+
render(<InstallPrompt onInstall={vi.fn()} />);
|
|
241
|
+
fireEvent(window, createPromptEvent('dismissed'));
|
|
242
|
+
|
|
243
|
+
await waitFor(() => {
|
|
244
|
+
expect(screen.getByTestId('install-prompt-install')).toBeInTheDocument();
|
|
245
|
+
});
|
|
246
|
+
fireEvent.click(screen.getByTestId('install-prompt-install'));
|
|
247
|
+
|
|
248
|
+
await waitFor(() => {
|
|
249
|
+
expect(screen.queryByTestId('install-prompt')).not.toBeInTheDocument();
|
|
250
|
+
});
|
|
251
|
+
expect(readDismissedAt(STORAGE_KEY)).not.toBeNull();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('hides and persists the dismissal when the close button is used', async () => {
|
|
255
|
+
const onDismiss = vi.fn();
|
|
256
|
+
|
|
257
|
+
render(<InstallPrompt onDismiss={onDismiss} />);
|
|
258
|
+
fireEvent(window, createPromptEvent('accepted'));
|
|
259
|
+
|
|
260
|
+
await waitFor(() => {
|
|
261
|
+
expect(screen.getByTestId('install-prompt-dismiss')).toBeInTheDocument();
|
|
262
|
+
});
|
|
263
|
+
fireEvent.click(screen.getByTestId('install-prompt-dismiss'));
|
|
264
|
+
|
|
265
|
+
await waitFor(() => {
|
|
266
|
+
expect(screen.queryByTestId('install-prompt')).not.toBeInTheDocument();
|
|
267
|
+
});
|
|
268
|
+
expect(onDismiss).toHaveBeenCalledTimes(1);
|
|
269
|
+
expect(readDismissedAt(STORAGE_KEY)).not.toBeNull();
|
|
270
|
+
});
|
|
271
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { InstallPrompt } from './InstallPrompt';
|
|
2
|
+
export { usePwaInstall } from './usePwaInstall';
|
|
3
|
+
export type {
|
|
4
|
+
BeforeInstallPromptEvent,
|
|
5
|
+
InstallOutcome,
|
|
6
|
+
InstallPlatform,
|
|
7
|
+
InstallPromptProps,
|
|
8
|
+
PwaInstallState,
|
|
9
|
+
UsePwaInstallOptions,
|
|
10
|
+
} from './InstallPrompt.types';
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_DISMISS_DAYS,
|
|
5
|
+
DEFAULT_STORAGE_KEY,
|
|
6
|
+
isDismissalActive,
|
|
7
|
+
isStandaloneDisplay,
|
|
8
|
+
readDismissedAt,
|
|
9
|
+
resolveInstallPlatform,
|
|
10
|
+
writeDismissedAt,
|
|
11
|
+
} from './InstallPrompt.helpers';
|
|
12
|
+
import type {
|
|
13
|
+
BeforeInstallPromptEvent,
|
|
14
|
+
InstallOutcome,
|
|
15
|
+
InstallPlatform,
|
|
16
|
+
PwaInstallState,
|
|
17
|
+
UsePwaInstallOptions,
|
|
18
|
+
} from './InstallPrompt.types';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Headless PWA install machinery.
|
|
22
|
+
*
|
|
23
|
+
* A manifest and a service worker make a site installABLE; they do not make it
|
|
24
|
+
* install. Chromium announces its willingness by firing `beforeinstallprompt`
|
|
25
|
+
* and then does nothing further on its own beyond a small address-bar icon most
|
|
26
|
+
* users never notice. Unless a page calls `preventDefault()` on that event and
|
|
27
|
+
* keeps it, the handle is gone and the app can never ask.
|
|
28
|
+
*
|
|
29
|
+
* That is the whole reason this hook exists. It captures the event, tracks the
|
|
30
|
+
* three states that should suppress an install invitation (already installed,
|
|
31
|
+
* recently dismissed, browser cannot install), and hands back a
|
|
32
|
+
* gesture-callable `promptInstall`.
|
|
33
|
+
*
|
|
34
|
+
* Every environment probe runs in an effect rather than in a state initialiser:
|
|
35
|
+
* touching `window` during render breaks SSR and desynchronises hydration, so
|
|
36
|
+
* the first paint deliberately reports `canInstall: false` and corrects itself
|
|
37
|
+
* on mount.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/** Tracks whether the page is running as an installed app. */
|
|
41
|
+
const useInstalledState = (onInstalled?: () => void) => {
|
|
42
|
+
const [isInstalled, setIsInstalled] = useState(false);
|
|
43
|
+
|
|
44
|
+
// Held in a ref, and refreshed in its own effect rather than during render, so
|
|
45
|
+
// that a caller passing an inline callback does not re-subscribe the listener
|
|
46
|
+
// on every render.
|
|
47
|
+
const onInstalledRef = useRef(onInstalled);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
onInstalledRef.current = onInstalled;
|
|
50
|
+
}, [onInstalled]);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
setIsInstalled(isStandaloneDisplay());
|
|
54
|
+
|
|
55
|
+
const markInstalled = () => {
|
|
56
|
+
setIsInstalled(true);
|
|
57
|
+
onInstalledRef.current?.();
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
window.addEventListener('appinstalled', markInstalled);
|
|
61
|
+
return () => window.removeEventListener('appinstalled', markInstalled);
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
return isInstalled;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Captures and holds the deferred `beforeinstallprompt` event.
|
|
69
|
+
*
|
|
70
|
+
* The listener is attached on mount, which is normally AFTER the browser would
|
|
71
|
+
* have fired the event on a cold load. Chromium re-fires it on navigation and
|
|
72
|
+
* on re-evaluation of the install criteria, so a missed first event resolves
|
|
73
|
+
* itself; a host that needs the very first one can attach an early capture in
|
|
74
|
+
* its HTML entry point and re-dispatch it.
|
|
75
|
+
*/
|
|
76
|
+
const useDeferredPrompt = () => {
|
|
77
|
+
const deferredRef = useRef<BeforeInstallPromptEvent | null>(null);
|
|
78
|
+
const [promptReady, setPromptReady] = useState(false);
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const capture = (event: Event) => {
|
|
82
|
+
// Without preventDefault the browser owns the event and the handle dies
|
|
83
|
+
// with it — this is the line that makes an in-page install button possible.
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
deferredRef.current = event as BeforeInstallPromptEvent;
|
|
86
|
+
setPromptReady(true);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
window.addEventListener('beforeinstallprompt', capture);
|
|
90
|
+
return () => window.removeEventListener('beforeinstallprompt', capture);
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
const clearPrompt = useCallback(() => {
|
|
94
|
+
deferredRef.current = null;
|
|
95
|
+
setPromptReady(false);
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
return { deferredRef, promptReady, clearPrompt };
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** Reads and writes the "don't ask again for a while" record. */
|
|
102
|
+
const useDismissal = (storageKey: string, dismissForDays: number) => {
|
|
103
|
+
const [dismissed, setDismissed] = useState(false);
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
const dismissedAt = readDismissedAt(storageKey);
|
|
107
|
+
setDismissed(isDismissalActive(dismissedAt, dismissForDays, Date.now()));
|
|
108
|
+
}, [storageKey, dismissForDays]);
|
|
109
|
+
|
|
110
|
+
const persistDismissal = useCallback(() => {
|
|
111
|
+
writeDismissedAt(storageKey, Date.now());
|
|
112
|
+
setDismissed(true);
|
|
113
|
+
}, [storageKey]);
|
|
114
|
+
|
|
115
|
+
return { dismissed, persistDismissal };
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** Resolves the install route once the deferred-prompt state is known. */
|
|
119
|
+
const useInstallPlatform = (promptReady: boolean): InstallPlatform => {
|
|
120
|
+
const [platform, setPlatform] = useState<InstallPlatform>('unsupported');
|
|
121
|
+
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
setPlatform(resolveInstallPlatform(promptReady));
|
|
124
|
+
}, [promptReady]);
|
|
125
|
+
|
|
126
|
+
return platform;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const usePwaInstall = (options: UsePwaInstallOptions = {}): PwaInstallState => {
|
|
130
|
+
const {
|
|
131
|
+
storageKey = DEFAULT_STORAGE_KEY,
|
|
132
|
+
dismissForDays = DEFAULT_DISMISS_DAYS,
|
|
133
|
+
onInstalled,
|
|
134
|
+
} = options;
|
|
135
|
+
|
|
136
|
+
const isInstalled = useInstalledState(onInstalled);
|
|
137
|
+
const { deferredRef, promptReady, clearPrompt } = useDeferredPrompt();
|
|
138
|
+
const { dismissed, persistDismissal } = useDismissal(storageKey, dismissForDays);
|
|
139
|
+
const platform = useInstallPlatform(promptReady);
|
|
140
|
+
|
|
141
|
+
const promptInstall = useCallback(async (): Promise<InstallOutcome> => {
|
|
142
|
+
const deferred = deferredRef.current;
|
|
143
|
+
if (!deferred) return 'unavailable';
|
|
144
|
+
|
|
145
|
+
await deferred.prompt();
|
|
146
|
+
const { outcome } = await deferred.userChoice;
|
|
147
|
+
|
|
148
|
+
// Single-use handle: spent whichever way the user answered. Chromium emits a
|
|
149
|
+
// fresh event if the app is still installable, so holding this one would
|
|
150
|
+
// only mean calling prompt() twice on a dead event.
|
|
151
|
+
clearPrompt();
|
|
152
|
+
|
|
153
|
+
// Declining the browser's own dialog is a real "no" — record it, or the
|
|
154
|
+
// affordance reappears on the next render and badgers the user.
|
|
155
|
+
if (outcome === 'dismissed') persistDismissal();
|
|
156
|
+
|
|
157
|
+
return outcome;
|
|
158
|
+
}, [deferredRef, clearPrompt, persistDismissal]);
|
|
159
|
+
|
|
160
|
+
// iOS has no event to wait for, so its affordance is gated on the platform
|
|
161
|
+
// probe alone; everywhere else a live captured prompt is the prerequisite.
|
|
162
|
+
const canInstall = !isInstalled && !dismissed && (platform === 'ios' || promptReady);
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
canInstall,
|
|
166
|
+
platform,
|
|
167
|
+
isInstalled,
|
|
168
|
+
promptInstall,
|
|
169
|
+
dismiss: persistDismissal,
|
|
170
|
+
};
|
|
171
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Theme } from '@mui/material';
|
|
2
|
+
|
|
3
|
+
import type { CustomTransitionProps, TransitionVariant } from './Transition.types';
|
|
4
|
+
|
|
5
|
+
type Duration = NonNullable<CustomTransitionProps['duration']>;
|
|
6
|
+
type Easing = NonNullable<CustomTransitionProps['easing']>;
|
|
7
|
+
|
|
8
|
+
/** An explicit `duration` wins in either form; otherwise the variant picks one. */
|
|
9
|
+
export const transitionDuration = (
|
|
10
|
+
theme: Theme,
|
|
11
|
+
variant: TransitionVariant,
|
|
12
|
+
duration?: Duration,
|
|
13
|
+
): Duration => {
|
|
14
|
+
if (typeof duration === 'number' || typeof duration === 'object') return duration;
|
|
15
|
+
|
|
16
|
+
switch (variant) {
|
|
17
|
+
case 'slide':
|
|
18
|
+
return theme.transitions.duration.enteringScreen;
|
|
19
|
+
case 'fade':
|
|
20
|
+
case 'scale':
|
|
21
|
+
case 'grow':
|
|
22
|
+
case 'zoom':
|
|
23
|
+
return theme.transitions.duration.shorter;
|
|
24
|
+
default:
|
|
25
|
+
return theme.transitions.duration.standard;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** As above: an explicit `easing` wins, otherwise the variant picks one. */
|
|
30
|
+
export const transitionEasing = (
|
|
31
|
+
theme: Theme,
|
|
32
|
+
variant: TransitionVariant,
|
|
33
|
+
easing?: Easing,
|
|
34
|
+
): Easing => {
|
|
35
|
+
if (typeof easing === 'string' || typeof easing === 'object') return easing;
|
|
36
|
+
|
|
37
|
+
return variant === 'slide'
|
|
38
|
+
? theme.transitions.easing.easeOut
|
|
39
|
+
: theme.transitions.easing.easeInOut;
|
|
40
|
+
};
|