@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
|
@@ -22,6 +22,7 @@ export * from './Popover';
|
|
|
22
22
|
export * from './Progress';
|
|
23
23
|
export * from './SectionOnboarding/SectionOnboarding';
|
|
24
24
|
export * from './Sheet';
|
|
25
|
+
export * from './StatCard';
|
|
25
26
|
export * from './Stepper/Stepper';
|
|
26
27
|
export * from './Table';
|
|
27
28
|
export * from './Timeline';
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { ConfirmHandler, UseConfirmActionResult } from './ConfirmAction.types';
|
|
4
|
+
|
|
5
|
+
/** Used when the guarded action rejects and the caller supplied no wording. */
|
|
6
|
+
export const DEFAULT_ERROR_TEXT = 'Não foi possível concluir a ação. Tente novamente.';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* What the popup says after a failed attempt.
|
|
10
|
+
*
|
|
11
|
+
* A rejection carrying a message wins: the server's sentence ("esta categoria
|
|
12
|
+
* ainda tem produtos vinculados") is the one the operator can act on, and it is
|
|
13
|
+
* strictly better than the generic fallback. Handlers that have nothing useful
|
|
14
|
+
* to say reject with nothing and get `errorText`.
|
|
15
|
+
*/
|
|
16
|
+
function failureMessage(cause: unknown, errorText: string): string {
|
|
17
|
+
if (cause instanceof Error && cause.message) return cause.message;
|
|
18
|
+
return errorText;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The confirmation state machine, headless.
|
|
23
|
+
*
|
|
24
|
+
* It is the whole safety contract in one place, so every consumer gets the same
|
|
25
|
+
* behaviour: the action runs ONLY from `confirm`, never from opening the popup;
|
|
26
|
+
* a second confirm while the first is in flight is dropped (a double-click
|
|
27
|
+
* cannot send two deletes); a rejection keeps the popup open carrying the
|
|
28
|
+
* failure instead of closing over a write that never happened.
|
|
29
|
+
*/
|
|
30
|
+
export function useConfirmAction(
|
|
31
|
+
onConfirm: ConfirmHandler,
|
|
32
|
+
errorText: string = DEFAULT_ERROR_TEXT,
|
|
33
|
+
): UseConfirmActionResult {
|
|
34
|
+
const [open, setOpen] = useState(false);
|
|
35
|
+
const [pending, setPending] = useState(false);
|
|
36
|
+
const [error, setError] = useState<string | null>(null);
|
|
37
|
+
|
|
38
|
+
// `pending` is mirrored in a ref because the two guards that matter — drop a
|
|
39
|
+
// second confirm, ignore a cancel mid-flight — are read inside callbacks that
|
|
40
|
+
// must not be rebuilt on every state change. Reading the state variable there
|
|
41
|
+
// would leave a stale closure holding `false` for the whole in-flight window,
|
|
42
|
+
// which is exactly the double-submit this component exists to prevent.
|
|
43
|
+
const busy = useRef(false);
|
|
44
|
+
|
|
45
|
+
// The action can settle after the trigger has unmounted (a row deleted from
|
|
46
|
+
// under its own menu), so the settle path must not touch state by then.
|
|
47
|
+
const mounted = useRef(true);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
mounted.current = true;
|
|
50
|
+
return () => {
|
|
51
|
+
mounted.current = false;
|
|
52
|
+
};
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
const request = useCallback((): void => {
|
|
56
|
+
setError(null);
|
|
57
|
+
setOpen(true);
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
const cancel = useCallback((): void => {
|
|
61
|
+
// Inert mid-flight: the write has already left, and closing here would hide
|
|
62
|
+
// its outcome.
|
|
63
|
+
if (busy.current) return;
|
|
64
|
+
setOpen(false);
|
|
65
|
+
setError(null);
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
const settle = useCallback((failure: string | null): void => {
|
|
69
|
+
busy.current = false;
|
|
70
|
+
if (!mounted.current) return;
|
|
71
|
+
setPending(false);
|
|
72
|
+
setError(failure);
|
|
73
|
+
if (!failure) setOpen(false);
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
const confirm = useCallback((): void => {
|
|
77
|
+
if (busy.current) return;
|
|
78
|
+
busy.current = true;
|
|
79
|
+
setPending(true);
|
|
80
|
+
setError(null);
|
|
81
|
+
// The handler is invoked synchronously, exactly as a plain `onClick` would
|
|
82
|
+
// be — only the OUTCOME is awaited. The try/catch is what puts a handler
|
|
83
|
+
// that throws synchronously on the same failure path as one that returns a
|
|
84
|
+
// rejected promise.
|
|
85
|
+
try {
|
|
86
|
+
const result = onConfirm();
|
|
87
|
+
void Promise.resolve(result)
|
|
88
|
+
.then(() => settle(null))
|
|
89
|
+
.catch((cause: unknown) => settle(failureMessage(cause, errorText)));
|
|
90
|
+
} catch (cause) {
|
|
91
|
+
settle(failureMessage(cause, errorText));
|
|
92
|
+
}
|
|
93
|
+
}, [errorText, onConfirm, settle]);
|
|
94
|
+
|
|
95
|
+
return { open, pending, error, request, cancel, confirm };
|
|
96
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# ConfirmAction
|
|
2
|
+
|
|
3
|
+
The confirmation popup for destructive actions — one implementation, so every
|
|
4
|
+
delete in the product behaves the same way.
|
|
5
|
+
|
|
6
|
+
It exists because the admin's destructive actions used to fire on a single
|
|
7
|
+
click: picking "Excluir" in a card's 3-dots menu called the endpoint
|
|
8
|
+
immediately, and a mis-tap on a phone destroyed the row. The few screens that
|
|
9
|
+
did confirm had each hand-rolled their own dialog, so the wording, the focus
|
|
10
|
+
order and the double-click behaviour all differed.
|
|
11
|
+
|
|
12
|
+
## What it guarantees
|
|
13
|
+
|
|
14
|
+
Whichever of the three entry points you use, you get the same contract:
|
|
15
|
+
|
|
16
|
+
- **Nothing runs until the operator confirms.** Opening the popup has no side
|
|
17
|
+
effect; the guarded handler is called from the confirm button and nowhere else.
|
|
18
|
+
- **One write per confirmation.** A returned promise holds the popup in its
|
|
19
|
+
pending state, and a second confirm during that window is dropped — a
|
|
20
|
+
double-click cannot send two DELETEs.
|
|
21
|
+
- **A failure stays visible.** If the action rejects, the popup stays open
|
|
22
|
+
carrying the error instead of closing over a write that never landed, and the
|
|
23
|
+
operator can retry from there. A rejection with a message shows that message
|
|
24
|
+
— throw the server's sentence (`throw new Error(result.error)`) and the
|
|
25
|
+
operator reads what actually blocked the write; `errorText` is the fallback.
|
|
26
|
+
- **The keyboard is safe.** Destructive popups open with focus on *Cancel*, so a
|
|
27
|
+
stray Enter still on the trigger cannot complete the action. Escape and the
|
|
28
|
+
backdrop cancel; both are inert while a write is in flight.
|
|
29
|
+
- **The popup names the thing.** `entityName` is echoed into the body, so it
|
|
30
|
+
reads "Bebidas", not "este item".
|
|
31
|
+
|
|
32
|
+
## Three entry points
|
|
33
|
+
|
|
34
|
+
### `withConfirmation(Component, defaults?)` — the HOC
|
|
35
|
+
|
|
36
|
+
The path of least resistance for an existing button: the wrapped component's own
|
|
37
|
+
`onClick` becomes the guarded action, so the call site keeps its shape.
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { withConfirmation } from '@12-apps/ui/feedback/ConfirmAction';
|
|
41
|
+
import { Button } from '@12-apps/ui/form/Button';
|
|
42
|
+
|
|
43
|
+
const DeleteButton = withConfirmation(Button, {
|
|
44
|
+
confirmText: 'Excluir',
|
|
45
|
+
description: 'Vai para a lixeira e pode ser restaurado de lá.',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
<DeleteButton
|
|
49
|
+
color="danger"
|
|
50
|
+
onClick={() => deleteCategoryAction({ tenantSlug, id: row.id })}
|
|
51
|
+
confirm={{ title: 'Excluir a categoria?', entityName: row.name }}
|
|
52
|
+
>
|
|
53
|
+
Excluir
|
|
54
|
+
</DeleteButton>;
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Defaults baked into the wrapper are merged with each instance's `confirm` prop,
|
|
58
|
+
instance wins. `confirm={false}` falls through to the plain component — the
|
|
59
|
+
escape hatch for the same button in a context where the action is harmless.
|
|
60
|
+
|
|
61
|
+
Refs are not forwarded (the HOC renders trigger + popup as a fragment, so there
|
|
62
|
+
is no single node to hand back). Use `<ConfirmAction>` where a ref is needed.
|
|
63
|
+
|
|
64
|
+
### `ConfirmButton` — the HOC, pre-applied
|
|
65
|
+
|
|
66
|
+
`withConfirmation(Button)` with the shared wording already in place. Use it when
|
|
67
|
+
you just want a guarded button and have no reason to build your own wrapper.
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { ConfirmButton } from '@12-apps/ui/feedback/ConfirmAction';
|
|
71
|
+
|
|
72
|
+
<ConfirmButton
|
|
73
|
+
color="danger"
|
|
74
|
+
onClick={remove}
|
|
75
|
+
confirm={{ title: 'Remover o domínio?', entityName: domain.host, confirmText: 'Remover' }}
|
|
76
|
+
>
|
|
77
|
+
Remover
|
|
78
|
+
</ConfirmButton>;
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### `<ConfirmAction>` — the render prop
|
|
82
|
+
|
|
83
|
+
For triggers that are not buttons: a dropdown item, a table row action, an icon.
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
<ConfirmAction
|
|
87
|
+
title="Excluir a mesa?"
|
|
88
|
+
entityName={row.label}
|
|
89
|
+
confirmText="Excluir"
|
|
90
|
+
onConfirm={() => deleteTableAction({ tenantSlug, id: row.id })}
|
|
91
|
+
>
|
|
92
|
+
{(request) => <MenuItem onClick={request}>Excluir</MenuItem>}
|
|
93
|
+
</ConfirmAction>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### `useConfirmAction(handler, errorText?)` — headless
|
|
97
|
+
|
|
98
|
+
The state machine on its own (`open`, `pending`, `error`, `request`, `cancel`,
|
|
99
|
+
`confirm`), for a popup that has to look like something else entirely. Pair it
|
|
100
|
+
with `<ConfirmActionDialog state={…} />` or your own surface.
|
|
101
|
+
|
|
102
|
+
## Writing the wording
|
|
103
|
+
|
|
104
|
+
- `title` is the question — "Excluir a categoria?".
|
|
105
|
+
- `confirmText` is the **verb**, never "OK". It is required for that reason: a
|
|
106
|
+
button reading "Confirmar" tells the operator nothing about what is about to
|
|
107
|
+
happen.
|
|
108
|
+
- `description` should say whether the action can be undone. A soft delete goes
|
|
109
|
+
to the recycle bin and comes back; a purge does not. Say which. Without one,
|
|
110
|
+
the popup falls back to naming the affected row.
|
|
111
|
+
- `typeToConfirm` requires the operator to type an exact string before the
|
|
112
|
+
confirm enables. Reserve it for actions with **no** recovery path — a
|
|
113
|
+
permanent purge, suspending a live store. Everywhere else it is friction that
|
|
114
|
+
buys nothing.
|
|
115
|
+
|
|
116
|
+
## Props
|
|
117
|
+
|
|
118
|
+
| Prop | Type | Default | Notes |
|
|
119
|
+
| --- | --- | --- | --- |
|
|
120
|
+
| `title` | `string` | — | Required. The question. |
|
|
121
|
+
| `confirmText` | `string` | — | Required. The verb on the confirm button. |
|
|
122
|
+
| `description` | `ReactNode` | derived from `entityName` | The consequence. |
|
|
123
|
+
| `entityName` | `string` | — | The row being acted on, echoed into the body. |
|
|
124
|
+
| `cancelText` | `string` | `Cancelar` | |
|
|
125
|
+
| `typeToConfirm` | `string` | — | Gates the confirm until typed exactly. |
|
|
126
|
+
| `typeToConfirmLabel` | `string` | derived | Label above the field. |
|
|
127
|
+
| `tone` | `'destructive' \| 'default'` | `destructive` | Colour + default focus. |
|
|
128
|
+
| `initialFocus` | `'confirm' \| 'cancel'` | `cancel` when destructive | |
|
|
129
|
+
| `errorText` | `string` | generic pt-BR sentence | Shown when the action rejects. |
|
|
130
|
+
| `dataTestId` | `string` | `confirm-action` | The popup's parts derive theirs from it. |
|
|
131
|
+
|
|
132
|
+
## Test ids
|
|
133
|
+
|
|
134
|
+
`dataTestId` (default `confirm-action`) is the root; the parts follow
|
|
135
|
+
`AlertDialog`'s convention:
|
|
136
|
+
|
|
137
|
+
- `confirm-action-confirm-button`
|
|
138
|
+
- `confirm-action-cancel-button`
|
|
139
|
+
- `confirm-action-error`
|
|
140
|
+
- `confirm-action-type-to-confirm` (on the input itself)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Alert, Stack, TextField, Typography } from '@mui/material';
|
|
2
|
+
import React, { type ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
import { AlertDialog } from '../../data-display/AlertDialog';
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ConfirmInitialFocus,
|
|
8
|
+
ConfirmOptions,
|
|
9
|
+
ConfirmTone,
|
|
10
|
+
UseConfirmActionResult,
|
|
11
|
+
} from './ConfirmAction.types';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_CANCEL_TEXT = 'Cancelar';
|
|
14
|
+
const DEFAULT_TEST_ID = 'confirm-action';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The popup's body: the row being acted on, then what happens to it.
|
|
18
|
+
*
|
|
19
|
+
* The NAME is rendered in its own right rather than woven into the sentence, so
|
|
20
|
+
* it is there whatever the caller wrote — "the popup names the specific entity"
|
|
21
|
+
* is the one thing that must not depend on each call site remembering to
|
|
22
|
+
* mention it. A body with neither a name nor a description would leave the
|
|
23
|
+
* operator with only the title to decide on, so a neutral fallback covers that.
|
|
24
|
+
*/
|
|
25
|
+
function confirmBody(description: ReactNode, entityName?: string): ReactNode {
|
|
26
|
+
if (!entityName) return description ?? "Esta ação não pode ser desfeita.";
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<Typography component="span" sx={{ fontWeight: 600, display: "block" }}>
|
|
30
|
+
{entityName}
|
|
31
|
+
</Typography>
|
|
32
|
+
{description}
|
|
33
|
+
</>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Destructive dialogs open on Cancel; anything else opens on the confirm. */
|
|
38
|
+
function resolveInitialFocus(
|
|
39
|
+
tone: ConfirmTone,
|
|
40
|
+
initialFocus?: ConfirmInitialFocus,
|
|
41
|
+
): ConfirmInitialFocus {
|
|
42
|
+
if (initialFocus) return initialFocus;
|
|
43
|
+
return tone === 'destructive' ? 'cancel' : 'confirm';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The type-to-confirm field. Rendered only when `typeToConfirm` is set, which
|
|
48
|
+
* should be rare: it is the guard for actions with NO recovery path, where a
|
|
49
|
+
* click is too cheap a gesture. Comparison is exact — trimmed of surrounding
|
|
50
|
+
* whitespace only, so a trailing space from a paste does not read as a mismatch.
|
|
51
|
+
*/
|
|
52
|
+
function TypeToConfirmField({
|
|
53
|
+
expected,
|
|
54
|
+
label,
|
|
55
|
+
value,
|
|
56
|
+
onChange,
|
|
57
|
+
disabled,
|
|
58
|
+
dataTestId,
|
|
59
|
+
}: {
|
|
60
|
+
expected: string;
|
|
61
|
+
label?: string;
|
|
62
|
+
value: string;
|
|
63
|
+
onChange: (next: string) => void;
|
|
64
|
+
disabled: boolean;
|
|
65
|
+
dataTestId: string;
|
|
66
|
+
}): React.ReactElement {
|
|
67
|
+
return (
|
|
68
|
+
<Stack spacing={1} sx={{ mt: 2 }}>
|
|
69
|
+
<Typography variant="body2" color="text.secondary">
|
|
70
|
+
{label ?? `Digite "${expected}" para confirmar.`}
|
|
71
|
+
</Typography>
|
|
72
|
+
<TextField
|
|
73
|
+
size="small"
|
|
74
|
+
value={value}
|
|
75
|
+
disabled={disabled}
|
|
76
|
+
onChange={(event) => onChange(event.target.value)}
|
|
77
|
+
// On the INPUT rather than the wrapper: a test id on MUI's TextField
|
|
78
|
+
// lands on the outer div, which is not the node a test can type into.
|
|
79
|
+
inputProps={{
|
|
80
|
+
'aria-label': label ?? `Digite ${expected} para confirmar`,
|
|
81
|
+
'data-testid': `${dataTestId}-type-to-confirm`,
|
|
82
|
+
}}
|
|
83
|
+
/>
|
|
84
|
+
</Stack>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ConfirmActionDialogProps extends ConfirmOptions {
|
|
89
|
+
state: UseConfirmActionResult;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The popup itself: an `AlertDialog` in its destructive skin, plus the two
|
|
94
|
+
* things a bare AlertDialog has no opinion about — the failure the last attempt
|
|
95
|
+
* produced (shown IN the dialog, which stays open, rather than closing over a
|
|
96
|
+
* write that never landed) and the optional type-to-confirm gate.
|
|
97
|
+
*/
|
|
98
|
+
export function ConfirmActionDialog({
|
|
99
|
+
state,
|
|
100
|
+
title,
|
|
101
|
+
description,
|
|
102
|
+
entityName,
|
|
103
|
+
confirmText,
|
|
104
|
+
cancelText = DEFAULT_CANCEL_TEXT,
|
|
105
|
+
typeToConfirm,
|
|
106
|
+
typeToConfirmLabel,
|
|
107
|
+
tone = 'destructive',
|
|
108
|
+
initialFocus,
|
|
109
|
+
dataTestId = DEFAULT_TEST_ID,
|
|
110
|
+
}: ConfirmActionDialogProps): React.ReactElement {
|
|
111
|
+
const [typed, setTyped] = React.useState('');
|
|
112
|
+
|
|
113
|
+
// Every fresh open starts from an empty field: carrying the previous answer
|
|
114
|
+
// over would hand the next delete a pre-satisfied gate.
|
|
115
|
+
React.useEffect(() => {
|
|
116
|
+
if (state.open) setTyped('');
|
|
117
|
+
}, [state.open]);
|
|
118
|
+
|
|
119
|
+
const gateUnmet = typeToConfirm !== undefined && typed.trim() !== typeToConfirm;
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<AlertDialog
|
|
123
|
+
open={state.open}
|
|
124
|
+
variant={tone === 'destructive' ? 'destructive' : 'default'}
|
|
125
|
+
title={title}
|
|
126
|
+
description={confirmBody(description, entityName)}
|
|
127
|
+
confirmText={confirmText}
|
|
128
|
+
cancelText={cancelText}
|
|
129
|
+
loading={state.pending}
|
|
130
|
+
confirmDisabled={gateUnmet}
|
|
131
|
+
initialFocus={resolveInitialFocus(tone, initialFocus)}
|
|
132
|
+
onConfirm={state.confirm}
|
|
133
|
+
onCancel={state.cancel}
|
|
134
|
+
onClose={state.cancel}
|
|
135
|
+
data-testid={dataTestId}
|
|
136
|
+
>
|
|
137
|
+
{state.error && (
|
|
138
|
+
<Alert severity="error" sx={{ mt: 1 }} data-testid={`${dataTestId}-error`}>
|
|
139
|
+
{state.error}
|
|
140
|
+
</Alert>
|
|
141
|
+
)}
|
|
142
|
+
{typeToConfirm !== undefined && (
|
|
143
|
+
<TypeToConfirmField
|
|
144
|
+
expected={typeToConfirm}
|
|
145
|
+
label={typeToConfirmLabel}
|
|
146
|
+
value={typed}
|
|
147
|
+
onChange={setTyped}
|
|
148
|
+
disabled={state.pending}
|
|
149
|
+
dataTestId={dataTestId}
|
|
150
|
+
/>
|
|
151
|
+
)}
|
|
152
|
+
</AlertDialog>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { Stack } from '@mui/material';
|
|
3
|
+
|
|
4
|
+
import { Button } from '../../form/Button';
|
|
5
|
+
|
|
6
|
+
import { ConfirmAction } from './ConfirmAction';
|
|
7
|
+
import { ConfirmButton } from './ConfirmButton';
|
|
8
|
+
import { withConfirmation } from './with-confirmation';
|
|
9
|
+
|
|
10
|
+
const meta: Meta<typeof ConfirmAction> = {
|
|
11
|
+
title: 'Feedback/ConfirmAction',
|
|
12
|
+
component: ConfirmAction,
|
|
13
|
+
parameters: {
|
|
14
|
+
layout: 'centered',
|
|
15
|
+
docs: {
|
|
16
|
+
description: {
|
|
17
|
+
component:
|
|
18
|
+
'Wraps a destructive action in a confirmation popup. The guarded handler runs only after the operator confirms; cancelling sends nothing, an in-flight write cannot be fired twice, and a failure keeps the popup open carrying the error.',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
tags: ['autodocs'],
|
|
23
|
+
argTypes: {
|
|
24
|
+
tone: { control: 'inline-radio', options: ['destructive', 'default'] },
|
|
25
|
+
initialFocus: { control: 'inline-radio', options: ['confirm', 'cancel'] },
|
|
26
|
+
typeToConfirm: { control: 'text' },
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default meta;
|
|
31
|
+
type Story = StoryObj<typeof meta>;
|
|
32
|
+
|
|
33
|
+
/** Resolves after a beat, so the popup's pending state is visible in the story. */
|
|
34
|
+
const slowWrite = () => new Promise<void>((resolve) => setTimeout(resolve, 1200));
|
|
35
|
+
|
|
36
|
+
export const Default: Story = {
|
|
37
|
+
args: {
|
|
38
|
+
title: 'Excluir a categoria?',
|
|
39
|
+
entityName: 'Bebidas',
|
|
40
|
+
description: 'Ela vai para a lixeira e pode ser restaurada de lá.',
|
|
41
|
+
confirmText: 'Excluir',
|
|
42
|
+
onConfirm: slowWrite,
|
|
43
|
+
children: (request) => (
|
|
44
|
+
<Button color="danger" onClick={request}>
|
|
45
|
+
Excluir
|
|
46
|
+
</Button>
|
|
47
|
+
),
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** No body of its own: the popup falls back to naming the row it affects. */
|
|
52
|
+
export const DescriptionFromEntityName: Story = {
|
|
53
|
+
args: {
|
|
54
|
+
...Default.args,
|
|
55
|
+
description: undefined,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** The non-destructive tone — same machinery, and focus opens on the confirm. */
|
|
60
|
+
export const NeutralTone: Story = {
|
|
61
|
+
args: {
|
|
62
|
+
...Default.args,
|
|
63
|
+
title: 'Publicar o cardápio?',
|
|
64
|
+
description: 'As alterações ficam visíveis na vitrine imediatamente.',
|
|
65
|
+
confirmText: 'Publicar',
|
|
66
|
+
tone: 'default',
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** For actions with no recovery path: the confirm stays dead until the name is typed. */
|
|
71
|
+
export const TypeToConfirm: Story = {
|
|
72
|
+
args: {
|
|
73
|
+
...Default.args,
|
|
74
|
+
title: 'Excluir definitivamente',
|
|
75
|
+
description: 'Este item e tudo o que depende dele somem para sempre.',
|
|
76
|
+
confirmText: 'Excluir definitivamente',
|
|
77
|
+
typeToConfirm: 'Bebidas',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** The write rejects: the popup stays open and says so, instead of closing silently. */
|
|
82
|
+
export const FailedWrite: Story = {
|
|
83
|
+
args: {
|
|
84
|
+
...Default.args,
|
|
85
|
+
onConfirm: () => Promise.reject(new Error('409')),
|
|
86
|
+
errorText: 'Esta categoria ainda tem produtos vinculados.',
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** The long label case — a title and body that wrap. */
|
|
91
|
+
export const LongContent: Story = {
|
|
92
|
+
args: {
|
|
93
|
+
...Default.args,
|
|
94
|
+
title: 'Remover este integrante da equipe e revogar todos os seus acessos?',
|
|
95
|
+
entityName: 'maria.fernanda.dossantos@exemplo.com.br',
|
|
96
|
+
description:
|
|
97
|
+
'A pessoa perde o acesso ao painel imediatamente e precisa de um novo convite para voltar. Os pedidos que ela registrou continuam no histórico da loja, atribuídos ao nome dela.',
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** The HOC path: an ordinary button, guarded by adding one prop. */
|
|
102
|
+
export const WithConfirmationHoc: StoryObj = {
|
|
103
|
+
render: () => (
|
|
104
|
+
<Stack direction="row" spacing={2}>
|
|
105
|
+
<ConfirmButton
|
|
106
|
+
color="danger"
|
|
107
|
+
onClick={slowWrite}
|
|
108
|
+
confirm={{
|
|
109
|
+
title: 'Excluir o desconto?',
|
|
110
|
+
entityName: 'Combo almoço',
|
|
111
|
+
description: 'O cupom para de valer na vitrine assim que for excluído.',
|
|
112
|
+
confirmText: 'Excluir',
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
Excluir
|
|
116
|
+
</ConfirmButton>
|
|
117
|
+
<ConfirmButton color="neutral" onClick={slowWrite} confirm={false}>
|
|
118
|
+
Sem confirmação
|
|
119
|
+
</ConfirmButton>
|
|
120
|
+
</Stack>
|
|
121
|
+
),
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** Wording baked into the wrapper once; each instance only says what differs. */
|
|
125
|
+
const DeleteButton = withConfirmation(Button, {
|
|
126
|
+
title: 'Excluir o item?',
|
|
127
|
+
confirmText: 'Excluir',
|
|
128
|
+
description: 'Ele vai para a lixeira e pode ser restaurado de lá.',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
export const HocWithBakedInDefaults: StoryObj = {
|
|
132
|
+
render: () => (
|
|
133
|
+
<Stack direction="row" spacing={2}>
|
|
134
|
+
<DeleteButton color="danger" onClick={slowWrite} confirm={{ entityName: 'Bebidas' }}>
|
|
135
|
+
Excluir categoria
|
|
136
|
+
</DeleteButton>
|
|
137
|
+
<DeleteButton
|
|
138
|
+
color="danger"
|
|
139
|
+
onClick={slowWrite}
|
|
140
|
+
confirm={{ entityName: 'Mesa 12', title: 'Excluir a mesa?' }}
|
|
141
|
+
>
|
|
142
|
+
Excluir mesa
|
|
143
|
+
</DeleteButton>
|
|
144
|
+
</Stack>
|
|
145
|
+
),
|
|
146
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { expect, fn, userEvent, within } from 'storybook/test';
|
|
3
|
+
|
|
4
|
+
import { Button } from '../../form/Button';
|
|
5
|
+
|
|
6
|
+
import { ConfirmAction } from './ConfirmAction';
|
|
7
|
+
import { ConfirmButton } from './ConfirmButton';
|
|
8
|
+
|
|
9
|
+
const meta: Meta<typeof ConfirmAction> = {
|
|
10
|
+
title: 'Feedback/ConfirmAction/Tests',
|
|
11
|
+
component: ConfirmAction,
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: 'centered',
|
|
14
|
+
chromatic: { disableSnapshot: false },
|
|
15
|
+
},
|
|
16
|
+
tags: ['autodocs', 'test', 'component:ConfirmAction'],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default meta;
|
|
20
|
+
type Story = StoryObj<typeof meta>;
|
|
21
|
+
|
|
22
|
+
const BASE = {
|
|
23
|
+
title: 'Excluir a categoria?',
|
|
24
|
+
description: 'Ela vai para a lixeira e pode ser restaurada de lá.',
|
|
25
|
+
confirmText: 'Excluir',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** The popup lives on `document.body`, so queries run against the whole screen. */
|
|
29
|
+
const body = (): ReturnType<typeof within> => within(document.body);
|
|
30
|
+
|
|
31
|
+
export const OpensWithoutRunningTheAction: Story = {
|
|
32
|
+
name: '🧪 Trigger opens the popup and runs nothing',
|
|
33
|
+
args: {
|
|
34
|
+
...BASE,
|
|
35
|
+
onConfirm: fn(),
|
|
36
|
+
children: (request) => (
|
|
37
|
+
<Button color="danger" onClick={request}>
|
|
38
|
+
Excluir
|
|
39
|
+
</Button>
|
|
40
|
+
),
|
|
41
|
+
},
|
|
42
|
+
play: async ({ args, canvasElement }) => {
|
|
43
|
+
await userEvent.click(within(canvasElement).getByRole('button', { name: 'Excluir' }));
|
|
44
|
+
|
|
45
|
+
await expect(body().getByText('Excluir a categoria?')).toBeInTheDocument();
|
|
46
|
+
await expect(args.onConfirm).not.toHaveBeenCalled();
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const CancelSendsNothing: Story = {
|
|
51
|
+
name: '🧪 Cancel sends nothing',
|
|
52
|
+
args: { ...OpensWithoutRunningTheAction.args, onConfirm: fn() },
|
|
53
|
+
play: async ({ args, canvasElement }) => {
|
|
54
|
+
await userEvent.click(within(canvasElement).getByRole('button', { name: 'Excluir' }));
|
|
55
|
+
await userEvent.click(body().getByTestId('confirm-action-cancel-button'));
|
|
56
|
+
|
|
57
|
+
await expect(args.onConfirm).not.toHaveBeenCalled();
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const ConfirmRunsTheAction: Story = {
|
|
62
|
+
name: '🧪 Confirm runs the action once',
|
|
63
|
+
args: { ...OpensWithoutRunningTheAction.args, onConfirm: fn() },
|
|
64
|
+
play: async ({ args, canvasElement }) => {
|
|
65
|
+
await userEvent.click(within(canvasElement).getByRole('button', { name: 'Excluir' }));
|
|
66
|
+
await userEvent.click(body().getByTestId('confirm-action-confirm-button'));
|
|
67
|
+
|
|
68
|
+
await expect(args.onConfirm).toHaveBeenCalledTimes(1);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const TypeToConfirmGate: Story = {
|
|
73
|
+
name: '🧪 Type-to-confirm gates the confirm button',
|
|
74
|
+
args: {
|
|
75
|
+
...OpensWithoutRunningTheAction.args,
|
|
76
|
+
onConfirm: fn(),
|
|
77
|
+
typeToConfirm: 'Bebidas',
|
|
78
|
+
},
|
|
79
|
+
play: async ({ canvasElement }) => {
|
|
80
|
+
await userEvent.click(within(canvasElement).getByRole('button', { name: 'Excluir' }));
|
|
81
|
+
|
|
82
|
+
await expect(body().getByTestId('confirm-action-confirm-button')).toBeDisabled();
|
|
83
|
+
await userEvent.type(body().getByTestId('confirm-action-type-to-confirm'), 'Bebidas');
|
|
84
|
+
await expect(body().getByTestId('confirm-action-confirm-button')).toBeEnabled();
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const KeyboardCancelsWithEscape: Story = {
|
|
89
|
+
name: '🧪 Escape closes without running the action',
|
|
90
|
+
args: { ...OpensWithoutRunningTheAction.args, onConfirm: fn() },
|
|
91
|
+
play: async ({ args, canvasElement }) => {
|
|
92
|
+
await userEvent.click(within(canvasElement).getByRole('button', { name: 'Excluir' }));
|
|
93
|
+
await userEvent.keyboard('{Escape}');
|
|
94
|
+
|
|
95
|
+
await expect(args.onConfirm).not.toHaveBeenCalled();
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const HocGuardsTheWrappedButton: StoryObj = {
|
|
100
|
+
name: '🧪 withConfirmation guards the wrapped button',
|
|
101
|
+
render: (_args, { args }) => (
|
|
102
|
+
<ConfirmButton color="danger" onClick={args.onClick} confirm={BASE}>
|
|
103
|
+
Excluir
|
|
104
|
+
</ConfirmButton>
|
|
105
|
+
),
|
|
106
|
+
args: { onClick: fn() },
|
|
107
|
+
play: async ({ args, canvasElement }) => {
|
|
108
|
+
await userEvent.click(within(canvasElement).getByRole('button', { name: 'Excluir' }));
|
|
109
|
+
await expect(args.onClick).not.toHaveBeenCalled();
|
|
110
|
+
|
|
111
|
+
await userEvent.click(body().getByTestId('confirm-action-confirm-button'));
|
|
112
|
+
await expect(args.onClick).toHaveBeenCalledTimes(1);
|
|
113
|
+
},
|
|
114
|
+
};
|