@12-apps/ui 1.0.0 → 1.3.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/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,113 @@
|
|
|
1
|
+
# InstallPrompt
|
|
2
|
+
|
|
3
|
+
A dismissible "install this app" invitation, plus the `usePwaInstall` hook behind it.
|
|
4
|
+
|
|
5
|
+
## The problem this solves
|
|
6
|
+
|
|
7
|
+
A manifest and a service worker make a site **installable**. They do not make it
|
|
8
|
+
**install**. Chromium signals its willingness by firing `beforeinstallprompt` and
|
|
9
|
+
then does nothing further on its own beyond a small address-bar icon most users
|
|
10
|
+
never notice. If no page code calls `preventDefault()` on that event and keeps a
|
|
11
|
+
reference to it, the handle is gone and the app can never ask.
|
|
12
|
+
|
|
13
|
+
So a site can pass every installability criterion a browser checks — HTTPS,
|
|
14
|
+
manifest, matching icons, a service worker with a fetch handler — and still never
|
|
15
|
+
offer to install, because nothing in it captures the event. That is the gap this
|
|
16
|
+
component fills.
|
|
17
|
+
|
|
18
|
+
On iOS Safari the event never fires at all: there is no programmatic install, only
|
|
19
|
+
Share → Add to Home Screen. Collapsing that into "unsupported" hides the affordance
|
|
20
|
+
from the largest slice of a mobile storefront's traffic, so it is handled as its own
|
|
21
|
+
case with instructions in place of a button.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { InstallPrompt } from '@12-apps/ui/utility/InstallPrompt';
|
|
27
|
+
|
|
28
|
+
export const StorefrontHeader = () => (
|
|
29
|
+
<InstallPrompt
|
|
30
|
+
title="Instalar o FutureDrink"
|
|
31
|
+
description="Peça mais rápido na próxima visita, direto da tela de início."
|
|
32
|
+
installLabel="Instalar"
|
|
33
|
+
dismissLabel="Dispensar"
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Mount it unconditionally. It returns `null` unless an install affordance should
|
|
39
|
+
genuinely be shown, so there is nothing to gate at the call site.
|
|
40
|
+
|
|
41
|
+
### Headless
|
|
42
|
+
|
|
43
|
+
When the built-in presentation does not fit, use the hook and render your own:
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
import { usePwaInstall } from '@12-apps/ui/utility/InstallPrompt';
|
|
47
|
+
|
|
48
|
+
const InstallLink = () => {
|
|
49
|
+
const { canInstall, platform, promptInstall } = usePwaInstall();
|
|
50
|
+
|
|
51
|
+
if (!canInstall) return null;
|
|
52
|
+
if (platform === 'ios') return <IosHint />;
|
|
53
|
+
|
|
54
|
+
return <button onClick={() => void promptInstall()}>Instalar</button>;
|
|
55
|
+
};
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`promptInstall()` must be called from a user gesture — browsers reject a prompt
|
|
59
|
+
raised from a timer or an effect.
|
|
60
|
+
|
|
61
|
+
## When it renders nothing
|
|
62
|
+
|
|
63
|
+
| Condition | Why |
|
|
64
|
+
|---|---|
|
|
65
|
+
| Already installed | `display-mode: standalone`, or `navigator.standalone` on iOS |
|
|
66
|
+
| Recently dismissed | A stored timestamp inside the `dismissForDays` window |
|
|
67
|
+
| No install route | Not Chromium, and not iOS Safari — e.g. Chrome on iOS, which cannot add to the home screen at all |
|
|
68
|
+
| Event not yet captured | Chromium has not (yet) offered a prompt |
|
|
69
|
+
|
|
70
|
+
## Props
|
|
71
|
+
|
|
72
|
+
| Prop | Type | Default | Description |
|
|
73
|
+
|---|---|---|---|
|
|
74
|
+
| `title` | `string` | `'Install this app'` | Headline text |
|
|
75
|
+
| `description` | `string` | — | Supporting line |
|
|
76
|
+
| `installLabel` | `string` | `'Install'` | Install button label (Chromium only) |
|
|
77
|
+
| `iosInstructions` | `ReactNode` | Share → Add to Home Screen | Replaces the built-in iOS wording |
|
|
78
|
+
| `dismissLabel` | `string` | `'Dismiss install prompt'` | Accessible label for the close button |
|
|
79
|
+
| `icon` | `ReactNode` | install glyph | Leading icon |
|
|
80
|
+
| `onInstall` | `(outcome: InstallOutcome) => void` | — | Called after the native prompt resolves |
|
|
81
|
+
| `onDismiss` | `() => void` | — | Called when dismissed |
|
|
82
|
+
| `storageKey` | `string` | `'pwa-install-dismissed'` | localStorage key for the dismissal |
|
|
83
|
+
| `dismissForDays` | `number` | `30` | Dismissal lifetime; `0` means permanent |
|
|
84
|
+
|
|
85
|
+
All copy defaults to English and every string is a prop — this package is
|
|
86
|
+
locale-neutral and the consuming app supplies its own wording.
|
|
87
|
+
|
|
88
|
+
## Hook API
|
|
89
|
+
|
|
90
|
+
`usePwaInstall(options)` returns:
|
|
91
|
+
|
|
92
|
+
- `canInstall` — whether to render an affordance at all
|
|
93
|
+
- `platform` — `'prompt'` (Chromium), `'ios'` (Safari on iOS/iPadOS), or `'unsupported'`
|
|
94
|
+
- `isInstalled` — running as an installed app
|
|
95
|
+
- `promptInstall()` — fires the native prompt; resolves `'accepted' | 'dismissed' | 'unavailable'`
|
|
96
|
+
- `dismiss()` — suppresses the affordance for `dismissForDays`
|
|
97
|
+
|
|
98
|
+
## Notes
|
|
99
|
+
|
|
100
|
+
- **SSR-safe.** Every environment probe runs in an effect, never during render, so
|
|
101
|
+
there is no hydration mismatch. The first paint reports `canInstall: false` and
|
|
102
|
+
corrects itself on mount.
|
|
103
|
+
- **The deferred event is single-use.** After `prompt()` resolves it is discarded;
|
|
104
|
+
Chromium emits a fresh one if the app is still installable.
|
|
105
|
+
- **Declining the native dialog counts as a dismissal**, or the affordance would
|
|
106
|
+
reappear on the next render.
|
|
107
|
+
- **Storage failures are swallowed.** `localStorage` throws rather than returning
|
|
108
|
+
`null` in Safari private mode and wherever cookies are blocked; an uncaught throw
|
|
109
|
+
would break the page on exactly the browsers this component serves.
|
|
110
|
+
- **A missed first event recovers on its own.** The listener attaches on mount,
|
|
111
|
+
which can be after a cold-load event. Chromium re-fires on navigation and on
|
|
112
|
+
re-evaluation of the criteria. A host that needs the very first one can capture
|
|
113
|
+
it in its HTML entry point and re-dispatch it once the app has mounted.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Box, Typography } from '@mui/material';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
import { InstallPrompt } from './InstallPrompt';
|
|
6
|
+
import type { BeforeInstallPromptEvent } from './InstallPrompt.types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The component listens for a browser event that Storybook will never fire, so
|
|
10
|
+
* every story synthesises one. This is also the shape a host can dispatch from
|
|
11
|
+
* its own early capture script to replay an event that arrived before React
|
|
12
|
+
* mounted.
|
|
13
|
+
*/
|
|
14
|
+
const useSyntheticInstallEvent = () => {
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const event = new Event('beforeinstallprompt') as BeforeInstallPromptEvent;
|
|
17
|
+
|
|
18
|
+
Object.assign(event, {
|
|
19
|
+
platforms: ['web'],
|
|
20
|
+
prompt: () => Promise.resolve(),
|
|
21
|
+
userChoice: Promise.resolve({ outcome: 'accepted' as const, platform: 'web' }),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
window.dispatchEvent(event);
|
|
25
|
+
}, []);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Storage is shared across stories; clear it so a dismissal in one does not hide the next. */
|
|
29
|
+
const useCleanDismissalState = (storageKey: string) => {
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
window.localStorage.removeItem(storageKey);
|
|
32
|
+
}, [storageKey]);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const meta: Meta<typeof InstallPrompt> = {
|
|
36
|
+
title: 'Utility/InstallPrompt',
|
|
37
|
+
component: InstallPrompt,
|
|
38
|
+
tags: ['autodocs', 'component:InstallPrompt'],
|
|
39
|
+
parameters: {
|
|
40
|
+
layout: 'padded',
|
|
41
|
+
docs: {
|
|
42
|
+
description: {
|
|
43
|
+
component:
|
|
44
|
+
'Dismissible PWA install invitation. Captures the Chromium `beforeinstallprompt` ' +
|
|
45
|
+
'event so the app can offer installation itself, and falls back to Add to Home ' +
|
|
46
|
+
'Screen instructions on iOS Safari, where no programmatic install exists. Renders ' +
|
|
47
|
+
'nothing when the app is already installed, was recently dismissed, or the browser ' +
|
|
48
|
+
'cannot install it.',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default meta;
|
|
55
|
+
type Story = StoryObj<typeof InstallPrompt>;
|
|
56
|
+
|
|
57
|
+
const DefaultComponent = () => {
|
|
58
|
+
useCleanDismissalState('sb-install-default');
|
|
59
|
+
useSyntheticInstallEvent();
|
|
60
|
+
|
|
61
|
+
return <InstallPrompt storageKey="sb-install-default" />;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const Default: Story = {
|
|
65
|
+
render: () => <DefaultComponent />,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const WithDescriptionComponent = () => {
|
|
69
|
+
useCleanDismissalState('sb-install-described');
|
|
70
|
+
useSyntheticInstallEvent();
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<InstallPrompt
|
|
74
|
+
storageKey="sb-install-described"
|
|
75
|
+
title="Install FutureDrink"
|
|
76
|
+
description="Order faster next time — the menu opens straight from your home screen."
|
|
77
|
+
installLabel="Install"
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const WithDescription: Story = {
|
|
83
|
+
render: () => <WithDescriptionComponent />,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const LocalisedComponent = () => {
|
|
87
|
+
useCleanDismissalState('sb-install-ptbr');
|
|
88
|
+
useSyntheticInstallEvent();
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<InstallPrompt
|
|
92
|
+
storageKey="sb-install-ptbr"
|
|
93
|
+
title="Instalar o FutureDrink"
|
|
94
|
+
description="Peça mais rápido na próxima visita, direto da tela de início."
|
|
95
|
+
installLabel="Instalar"
|
|
96
|
+
dismissLabel="Dispensar"
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** Every string is a prop; this package ships no locale of its own. */
|
|
102
|
+
export const Localised: Story = {
|
|
103
|
+
render: () => <LocalisedComponent />,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const NotInstallableComponent = () => (
|
|
107
|
+
<Box>
|
|
108
|
+
<Typography variant="body2" sx={{ mb: 2 }}>
|
|
109
|
+
No install event was dispatched, so the component renders nothing. This is what a browser
|
|
110
|
+
with no install route, an already-installed app, and a recent dismissal all look like.
|
|
111
|
+
</Typography>
|
|
112
|
+
<InstallPrompt storageKey="sb-install-absent" />
|
|
113
|
+
</Box>
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
export const NotInstallable: Story = {
|
|
117
|
+
render: () => <NotInstallableComponent />,
|
|
118
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { alpha } from '@mui/material';
|
|
2
|
+
import type { CSSObject, Theme } from '@mui/material/styles';
|
|
3
|
+
|
|
4
|
+
export const installPromptStyles = (theme: Theme): CSSObject => ({
|
|
5
|
+
display: 'flex',
|
|
6
|
+
alignItems: 'flex-start',
|
|
7
|
+
gap: theme.spacing(1.5),
|
|
8
|
+
padding: theme.spacing(1.5, 2),
|
|
9
|
+
borderRadius: theme.spacing(1),
|
|
10
|
+
backgroundColor: alpha(theme.palette.primary.main, 0.08),
|
|
11
|
+
border: `1px solid ${alpha(theme.palette.primary.main, 0.2)}`,
|
|
12
|
+
|
|
13
|
+
'.install-prompt-icon': {
|
|
14
|
+
flexShrink: 0,
|
|
15
|
+
marginTop: theme.spacing(0.25),
|
|
16
|
+
display: 'flex',
|
|
17
|
+
color: theme.palette.primary.main,
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
'.install-prompt-content': {
|
|
21
|
+
flex: 1,
|
|
22
|
+
minWidth: 0,
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
'.install-prompt-title': {
|
|
26
|
+
fontWeight: 600,
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
'.install-prompt-description': {
|
|
30
|
+
opacity: 0.9,
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
'.install-prompt-actions': {
|
|
34
|
+
display: 'flex',
|
|
35
|
+
flexShrink: 0,
|
|
36
|
+
alignItems: 'center',
|
|
37
|
+
gap: theme.spacing(1),
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
// The actions drop to their own full-width row on a phone, where this
|
|
41
|
+
// component does most of its work.
|
|
42
|
+
[theme.breakpoints.down('sm')]: {
|
|
43
|
+
flexWrap: 'wrap',
|
|
44
|
+
|
|
45
|
+
'.install-prompt-actions': {
|
|
46
|
+
width: '100%',
|
|
47
|
+
marginTop: theme.spacing(1),
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { expect, userEvent, waitFor, within } from 'storybook/test';
|
|
4
|
+
|
|
5
|
+
import { InstallPrompt } from './InstallPrompt';
|
|
6
|
+
import type { BeforeInstallPromptEvent } from './InstallPrompt.types';
|
|
7
|
+
|
|
8
|
+
const meta: Meta<typeof InstallPrompt> = {
|
|
9
|
+
title: 'Utility/InstallPrompt/Tests',
|
|
10
|
+
component: InstallPrompt,
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: 'padded',
|
|
13
|
+
chromatic: { disableSnapshot: false },
|
|
14
|
+
},
|
|
15
|
+
tags: ['autodocs', 'test', 'component:InstallPrompt'],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default meta;
|
|
19
|
+
export type Story = StoryObj<typeof meta>;
|
|
20
|
+
|
|
21
|
+
const dispatchInstallEvent = () => {
|
|
22
|
+
const event = new Event('beforeinstallprompt') as BeforeInstallPromptEvent;
|
|
23
|
+
|
|
24
|
+
Object.assign(event, {
|
|
25
|
+
platforms: ['web'],
|
|
26
|
+
prompt: () => Promise.resolve(),
|
|
27
|
+
userChoice: Promise.resolve({ outcome: 'accepted' as const, platform: 'web' }),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
window.dispatchEvent(event);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const Installable = ({ storageKey }: { storageKey: string }) => {
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
window.localStorage.removeItem(storageKey);
|
|
36
|
+
dispatchInstallEvent();
|
|
37
|
+
}, [storageKey]);
|
|
38
|
+
|
|
39
|
+
return <InstallPrompt storageKey={storageKey} title="Install this app" />;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ================================
|
|
43
|
+
// INTERACTION TESTS
|
|
44
|
+
// ================================
|
|
45
|
+
|
|
46
|
+
export const AppearsWhenInstallable: Story = {
|
|
47
|
+
render: () => <Installable storageKey="sb-test-appears" />,
|
|
48
|
+
play: async ({ canvasElement }) => {
|
|
49
|
+
const canvas = within(canvasElement);
|
|
50
|
+
|
|
51
|
+
await waitFor(async () => {
|
|
52
|
+
await expect(canvas.getByTestId('install-prompt')).toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
await expect(canvas.getByTestId('install-prompt-install')).toBeInTheDocument();
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const DismissHidesPrompt: Story = {
|
|
59
|
+
render: () => <Installable storageKey="sb-test-dismiss" />,
|
|
60
|
+
play: async ({ canvasElement }) => {
|
|
61
|
+
const canvas = within(canvasElement);
|
|
62
|
+
|
|
63
|
+
await waitFor(async () => {
|
|
64
|
+
await expect(canvas.getByTestId('install-prompt-dismiss')).toBeInTheDocument();
|
|
65
|
+
});
|
|
66
|
+
await userEvent.click(canvas.getByTestId('install-prompt-dismiss'));
|
|
67
|
+
|
|
68
|
+
await waitFor(async () => {
|
|
69
|
+
await expect(canvas.queryByTestId('install-prompt')).not.toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const HiddenWithoutInstallEvent: Story = {
|
|
75
|
+
render: () => <InstallPrompt storageKey="sb-test-hidden" />,
|
|
76
|
+
play: async ({ canvasElement }) => {
|
|
77
|
+
const canvas = within(canvasElement);
|
|
78
|
+
|
|
79
|
+
await waitFor(async () => {
|
|
80
|
+
await expect(canvas.queryByTestId('install-prompt')).not.toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import Close from '@mui/icons-material/Close';
|
|
2
|
+
import InstallMobile from '@mui/icons-material/InstallMobile';
|
|
3
|
+
import IosShare from '@mui/icons-material/IosShare';
|
|
4
|
+
import { Box, Button, IconButton, Typography } from '@mui/material';
|
|
5
|
+
import { styled } from '@mui/material/styles';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
import { installPromptStyles } from './InstallPrompt.styles';
|
|
9
|
+
import type { InstallPlatform, InstallPromptProps } from './InstallPrompt.types';
|
|
10
|
+
import { usePwaInstall } from './usePwaInstall';
|
|
11
|
+
|
|
12
|
+
const StyledInstallPrompt = styled(Box)(({ theme }) => installPromptStyles(theme));
|
|
13
|
+
|
|
14
|
+
const testIdFor = (base: string | undefined, suffix: string) =>
|
|
15
|
+
base ? `${base}-${suffix}` : `install-prompt-${suffix}`;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The iOS branch renders instructions rather than a button, because Safari
|
|
19
|
+
* exposes no API to open its own Add to Home Screen sheet. A button here would
|
|
20
|
+
* be a control that does nothing when pressed.
|
|
21
|
+
*/
|
|
22
|
+
const IosInstructions: React.FC<{
|
|
23
|
+
instructions?: React.ReactNode;
|
|
24
|
+
dataTestId?: string;
|
|
25
|
+
}> = ({ instructions, dataTestId }) => (
|
|
26
|
+
<Typography
|
|
27
|
+
className="install-prompt-description"
|
|
28
|
+
variant="body2"
|
|
29
|
+
component="div"
|
|
30
|
+
data-testid={testIdFor(dataTestId, 'ios-instructions')}
|
|
31
|
+
sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexWrap: 'wrap' }}
|
|
32
|
+
>
|
|
33
|
+
{instructions ?? (
|
|
34
|
+
<>
|
|
35
|
+
Tap <IosShare fontSize="small" aria-label="Share" /> then "Add to Home Screen"
|
|
36
|
+
</>
|
|
37
|
+
)}
|
|
38
|
+
</Typography>
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const InstallPromptBody: React.FC<{
|
|
42
|
+
icon?: React.ReactNode;
|
|
43
|
+
title: string;
|
|
44
|
+
description?: string;
|
|
45
|
+
iosInstructions?: React.ReactNode;
|
|
46
|
+
platform: InstallPlatform;
|
|
47
|
+
dataTestId?: string;
|
|
48
|
+
}> = ({ icon, title, description, iosInstructions, platform, dataTestId }) => (
|
|
49
|
+
<>
|
|
50
|
+
<Box className="install-prompt-icon" aria-hidden="true">
|
|
51
|
+
{icon ?? <InstallMobile />}
|
|
52
|
+
</Box>
|
|
53
|
+
|
|
54
|
+
<Box className="install-prompt-content">
|
|
55
|
+
<Typography
|
|
56
|
+
className="install-prompt-title"
|
|
57
|
+
variant="subtitle2"
|
|
58
|
+
component="div"
|
|
59
|
+
data-testid={testIdFor(dataTestId, 'title')}
|
|
60
|
+
>
|
|
61
|
+
{title}
|
|
62
|
+
</Typography>
|
|
63
|
+
|
|
64
|
+
{description && (
|
|
65
|
+
<Typography
|
|
66
|
+
className="install-prompt-description"
|
|
67
|
+
variant="body2"
|
|
68
|
+
component="div"
|
|
69
|
+
data-testid={testIdFor(dataTestId, 'description')}
|
|
70
|
+
>
|
|
71
|
+
{description}
|
|
72
|
+
</Typography>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
{platform === 'ios' && (
|
|
76
|
+
<IosInstructions instructions={iosInstructions} dataTestId={dataTestId} />
|
|
77
|
+
)}
|
|
78
|
+
</Box>
|
|
79
|
+
</>
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const InstallPromptActions: React.FC<{
|
|
83
|
+
platform: InstallPlatform;
|
|
84
|
+
installLabel: string;
|
|
85
|
+
dismissLabel: string;
|
|
86
|
+
onInstallClick: () => void;
|
|
87
|
+
onDismissClick: () => void;
|
|
88
|
+
dataTestId?: string;
|
|
89
|
+
}> = ({ platform, installLabel, dismissLabel, onInstallClick, onDismissClick, dataTestId }) => (
|
|
90
|
+
<Box className="install-prompt-actions">
|
|
91
|
+
{platform === 'prompt' && (
|
|
92
|
+
<Button
|
|
93
|
+
size="small"
|
|
94
|
+
variant="contained"
|
|
95
|
+
onClick={onInstallClick}
|
|
96
|
+
data-testid={testIdFor(dataTestId, 'install')}
|
|
97
|
+
>
|
|
98
|
+
{installLabel}
|
|
99
|
+
</Button>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
<IconButton
|
|
103
|
+
size="small"
|
|
104
|
+
onClick={onDismissClick}
|
|
105
|
+
aria-label={dismissLabel}
|
|
106
|
+
data-testid={testIdFor(dataTestId, 'dismiss')}
|
|
107
|
+
>
|
|
108
|
+
<Close fontSize="small" />
|
|
109
|
+
</IconButton>
|
|
110
|
+
</Box>
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A dismissible invitation to install the app, rendered only when the browser
|
|
115
|
+
* is actually able to install it and the user has not recently said no.
|
|
116
|
+
*
|
|
117
|
+
* Returns `null` in every other case — already installed, dismissed, or a
|
|
118
|
+
* browser with no install route at all — so it is safe to mount unconditionally
|
|
119
|
+
* in a layout rather than gating it at the call site.
|
|
120
|
+
*
|
|
121
|
+
* The copy defaults to English and every string is a prop: this package stays
|
|
122
|
+
* locale-neutral and the consuming app supplies its own wording.
|
|
123
|
+
*/
|
|
124
|
+
export const InstallPrompt = React.forwardRef<HTMLDivElement, InstallPromptProps>(
|
|
125
|
+
(
|
|
126
|
+
{
|
|
127
|
+
title = 'Install this app',
|
|
128
|
+
description,
|
|
129
|
+
installLabel = 'Install',
|
|
130
|
+
iosInstructions,
|
|
131
|
+
dismissLabel = 'Dismiss install prompt',
|
|
132
|
+
icon,
|
|
133
|
+
onInstall,
|
|
134
|
+
onDismiss,
|
|
135
|
+
storageKey,
|
|
136
|
+
dismissForDays,
|
|
137
|
+
className,
|
|
138
|
+
'data-testid': dataTestId = 'install-prompt',
|
|
139
|
+
...props
|
|
140
|
+
},
|
|
141
|
+
ref,
|
|
142
|
+
) => {
|
|
143
|
+
const { canInstall, platform, promptInstall, dismiss } = usePwaInstall({
|
|
144
|
+
storageKey,
|
|
145
|
+
dismissForDays,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const handleInstall = React.useCallback(() => {
|
|
149
|
+
void promptInstall().then((outcome) => onInstall?.(outcome));
|
|
150
|
+
}, [promptInstall, onInstall]);
|
|
151
|
+
|
|
152
|
+
const handleDismiss = React.useCallback(() => {
|
|
153
|
+
dismiss();
|
|
154
|
+
onDismiss?.();
|
|
155
|
+
}, [dismiss, onDismiss]);
|
|
156
|
+
|
|
157
|
+
if (!canInstall) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<StyledInstallPrompt
|
|
163
|
+
ref={ref}
|
|
164
|
+
className={className}
|
|
165
|
+
role="region"
|
|
166
|
+
aria-label={title}
|
|
167
|
+
data-testid={dataTestId}
|
|
168
|
+
{...props}
|
|
169
|
+
>
|
|
170
|
+
<InstallPromptBody
|
|
171
|
+
icon={icon}
|
|
172
|
+
title={title}
|
|
173
|
+
description={description}
|
|
174
|
+
iosInstructions={iosInstructions}
|
|
175
|
+
platform={platform}
|
|
176
|
+
dataTestId={dataTestId}
|
|
177
|
+
/>
|
|
178
|
+
|
|
179
|
+
<InstallPromptActions
|
|
180
|
+
platform={platform}
|
|
181
|
+
installLabel={installLabel}
|
|
182
|
+
dismissLabel={dismissLabel}
|
|
183
|
+
onInstallClick={handleInstall}
|
|
184
|
+
onDismissClick={handleDismiss}
|
|
185
|
+
dataTestId={dataTestId}
|
|
186
|
+
/>
|
|
187
|
+
</StyledInstallPrompt>
|
|
188
|
+
);
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
InstallPrompt.displayName = 'InstallPrompt';
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The Chromium-only event that makes a PWA installable from inside the page.
|
|
5
|
+
* It is NOT in TypeScript's DOM lib because it is not a standard: Safari and
|
|
6
|
+
* Firefox never fire it, so the spec never absorbed it.
|
|
7
|
+
*
|
|
8
|
+
* The event is a one-shot handle. Once `prompt()` resolves, the browser
|
|
9
|
+
* discards it; a later install attempt needs the NEXT event the browser emits,
|
|
10
|
+
* not this one held over.
|
|
11
|
+
*/
|
|
12
|
+
export interface BeforeInstallPromptEvent extends Event {
|
|
13
|
+
readonly platforms: string[];
|
|
14
|
+
readonly userChoice: Promise<{ outcome: 'accepted' | 'dismissed'; platform: string }>;
|
|
15
|
+
prompt: () => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* How — or whether — this browser can install the app.
|
|
20
|
+
*
|
|
21
|
+
* `ios` exists as its own case because iOS Safari can install but refuses to be
|
|
22
|
+
* asked: there is no programmatic route, only Share → Add to Home Screen. A
|
|
23
|
+
* host that collapses it into `unsupported` shows nothing to the largest slice
|
|
24
|
+
* of a mobile storefront's traffic.
|
|
25
|
+
*/
|
|
26
|
+
export type InstallPlatform = 'prompt' | 'ios' | 'unsupported';
|
|
27
|
+
|
|
28
|
+
/** Result of asking the browser to install. `unavailable` = there was no live prompt to fire. */
|
|
29
|
+
export type InstallOutcome = 'accepted' | 'dismissed' | 'unavailable';
|
|
30
|
+
|
|
31
|
+
export interface UsePwaInstallOptions {
|
|
32
|
+
/** localStorage key holding the dismissal timestamp. */
|
|
33
|
+
storageKey?: string;
|
|
34
|
+
/** Days a dismissal suppresses the prompt for. `0` suppresses it permanently. */
|
|
35
|
+
dismissForDays?: number;
|
|
36
|
+
/** Fired once the browser reports the app was installed. */
|
|
37
|
+
onInstalled?: () => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface PwaInstallState {
|
|
41
|
+
/** True only when an install affordance should actually be rendered. */
|
|
42
|
+
canInstall: boolean;
|
|
43
|
+
/** Which install route this browser offers. */
|
|
44
|
+
platform: InstallPlatform;
|
|
45
|
+
/** True when already running as an installed app. */
|
|
46
|
+
isInstalled: boolean;
|
|
47
|
+
/** Fires the native prompt. Must be called from a user gesture. */
|
|
48
|
+
promptInstall: () => Promise<InstallOutcome>;
|
|
49
|
+
/** Suppresses the affordance for `dismissForDays`. */
|
|
50
|
+
dismiss: () => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface InstallPromptProps {
|
|
54
|
+
/** Headline text. */
|
|
55
|
+
title?: string;
|
|
56
|
+
/** Supporting line under the title. */
|
|
57
|
+
description?: string;
|
|
58
|
+
/** Label for the install button (Chromium only). */
|
|
59
|
+
installLabel?: string;
|
|
60
|
+
/** Replaces the built-in iOS Share → Add to Home Screen wording. */
|
|
61
|
+
iosInstructions?: ReactNode;
|
|
62
|
+
/** Accessible label for the dismiss button. */
|
|
63
|
+
dismissLabel?: string;
|
|
64
|
+
/** Leading icon. Defaults to an install glyph. */
|
|
65
|
+
icon?: ReactNode;
|
|
66
|
+
/** Called with the outcome after the native prompt resolves. */
|
|
67
|
+
onInstall?: (outcome: InstallOutcome) => void;
|
|
68
|
+
/** Called when the user dismisses the prompt. */
|
|
69
|
+
onDismiss?: () => void;
|
|
70
|
+
/** localStorage key holding the dismissal timestamp. */
|
|
71
|
+
storageKey?: string;
|
|
72
|
+
/** Days a dismissal suppresses the prompt for. `0` suppresses it permanently. */
|
|
73
|
+
dismissForDays?: number;
|
|
74
|
+
/** Custom CSS class name. */
|
|
75
|
+
className?: string;
|
|
76
|
+
/** Test ID for component testing. */
|
|
77
|
+
'data-testid'?: string;
|
|
78
|
+
}
|