@12-apps/ui 1.0.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (448) hide show
  1. package/components-catalog.md +14 -0
  2. package/package.json +42 -27
  3. package/src/__tests__/data-grid.test.tsx +125 -0
  4. package/src/__tests__/package-wiring.test.ts +12 -8
  5. package/src/__tests__/social-login-button.test.tsx +5 -5
  6. package/src/__tests__/viewport.test.ts +50 -0
  7. package/src/charts/index.ts +8 -0
  8. package/src/components/data-display/Alert/Alert.md +1 -1
  9. package/src/components/data-display/Alert/Alert.stories.tsx +5 -7
  10. package/src/components/data-display/Alert/Alert.styles.tsx +255 -0
  11. package/src/components/data-display/Alert/Alert.test.stories.tsx +11 -13
  12. package/src/components/data-display/Alert/Alert.tsx +135 -328
  13. package/src/components/data-display/AlertDialog/AlertDialog.stories.tsx +3 -1
  14. package/src/components/data-display/AlertDialog/AlertDialog.tsx +191 -78
  15. package/src/components/data-display/AlertDialog/AlertDialog.types.ts +8 -1
  16. package/src/components/data-display/AsyncStateContainer/AsyncStateContainer.test.stories.tsx +5 -5
  17. package/src/components/data-display/AsyncStateContainer/AsyncStateContainer.tsx +44 -35
  18. package/src/components/data-display/Avatar/Avatar.stories.tsx +4 -1
  19. package/src/components/data-display/Avatar/Avatar.test.stories.tsx +5 -3
  20. package/src/components/data-display/Avatar/Avatar.tsx +154 -450
  21. package/src/components/data-display/Avatar/Avatar.view.tsx +396 -0
  22. package/src/components/data-display/Badge/Badge.animations.ts +67 -0
  23. package/src/components/data-display/Badge/Badge.helpers.ts +74 -0
  24. package/src/components/data-display/Badge/Badge.stories.tsx +14 -16
  25. package/src/components/data-display/Badge/Badge.styles.ts +261 -0
  26. package/src/components/data-display/Badge/Badge.test.stories.tsx +32 -31
  27. package/src/components/data-display/Badge/Badge.tsx +151 -557
  28. package/src/components/data-display/Badge/Badge.variants.ts +106 -0
  29. package/src/components/data-display/Badge/BadgeContent.tsx +107 -0
  30. package/src/components/data-display/Banner/Banner.stories.tsx +5 -1
  31. package/src/components/data-display/Banner/Banner.styles.ts +120 -0
  32. package/src/components/data-display/Banner/Banner.tsx +179 -224
  33. package/src/components/data-display/Chart/Chart.md +48 -1
  34. package/src/components/data-display/Chart/Chart.tsx +82 -500
  35. package/src/components/data-display/Chart/Chart.types.ts +2 -0
  36. package/src/components/data-display/Chart/ChartComposables.tsx +121 -0
  37. package/src/components/data-display/Chart/ChartSpec.types.ts +70 -0
  38. package/src/components/data-display/Chart/SpecChart.stories.tsx +150 -0
  39. package/src/components/data-display/Chart/SpecChart.test.stories.tsx +56 -0
  40. package/src/components/data-display/Chart/SpecChart.tsx +53 -0
  41. package/src/components/data-display/Chart/__tests__/SpecChart.test.tsx +60 -0
  42. package/src/components/data-display/Chart/__tests__/chart-format.test.ts +43 -0
  43. package/src/components/data-display/Chart/__tests__/chart-spec.test.ts +119 -0
  44. package/src/components/data-display/Chart/chart-format.ts +26 -0
  45. package/src/components/data-display/Chart/chart-internals.ts +148 -0
  46. package/src/components/data-display/Chart/chart-renderers.tsx +338 -0
  47. package/src/components/data-display/Chart/chart-spec-registry.ts +74 -0
  48. package/src/components/data-display/Chart/chart-spec.ts +171 -0
  49. package/src/components/data-display/Chart/index.ts +30 -0
  50. package/src/components/data-display/Chip/Chip.helpers.tsx +131 -0
  51. package/src/components/data-display/Chip/Chip.stories.tsx +2 -1
  52. package/src/components/data-display/Chip/Chip.test.stories.tsx +11 -8
  53. package/src/components/data-display/Chip/Chip.tsx +31 -94
  54. package/src/components/data-display/DataGrid/DataGrid.body.tsx +141 -0
  55. package/src/components/data-display/DataGrid/DataGrid.cells.tsx +238 -0
  56. package/src/components/data-display/DataGrid/DataGrid.model.ts +315 -0
  57. package/src/components/data-display/DataGrid/DataGrid.rows.ts +112 -0
  58. package/src/components/data-display/DataGrid/DataGrid.states.tsx +102 -0
  59. package/src/components/data-display/DataGrid/DataGrid.test.stories.tsx +6 -12
  60. package/src/components/data-display/DataGrid/DataGrid.tsx +129 -725
  61. package/src/components/data-display/DataViews/DataViewsGrid.tsx +187 -0
  62. package/src/components/data-display/DataViews/DataViewsTableBase.tsx +400 -0
  63. package/src/components/data-display/DataViews/ManageViewsDialog.tsx +140 -0
  64. package/src/components/data-display/DataViews/SaveViewModal.tsx +289 -0
  65. package/src/components/data-display/DataViews/__tests__/DataViewsGrid.test.tsx +271 -0
  66. package/src/components/data-display/DataViews/__tests__/DataViewsTableBase.test.tsx +134 -0
  67. package/src/components/data-display/DataViews/__tests__/SaveViewModal.test.tsx +83 -0
  68. package/src/components/data-display/DataViews/__tests__/data-views-filter.test.ts +115 -0
  69. package/src/components/data-display/DataViews/__tests__/data-views-preview.test.ts +53 -0
  70. package/src/components/data-display/DataViews/__tests__/range-pill.test.tsx +114 -0
  71. package/src/components/data-display/DataViews/base-card.stories.tsx +136 -0
  72. package/src/components/data-display/DataViews/base-card.tsx +278 -0
  73. package/src/components/data-display/DataViews/data-views-filter-panel.tsx +184 -0
  74. package/src/components/data-display/DataViews/data-views-filter.ts +138 -0
  75. package/src/components/data-display/DataViews/data-views-grid-helpers.tsx +248 -0
  76. package/src/components/data-display/DataViews/data-views-grid-parts.tsx +400 -0
  77. package/src/components/data-display/DataViews/data-views-inline-bar.tsx +257 -0
  78. package/src/components/data-display/DataViews/data-views-layout-context.tsx +176 -0
  79. package/src/components/data-display/DataViews/data-views-pagination.tsx +31 -0
  80. package/src/components/data-display/DataViews/data-views-preview.ts +53 -0
  81. package/src/components/data-display/DataViews/data-views-range-pill.tsx +277 -0
  82. package/src/components/data-display/DataViews/data-views-table-parts.tsx +150 -0
  83. package/src/components/data-display/DataViews/data-views-toolbar.tsx +149 -0
  84. package/src/components/data-display/DataViews/data-views-types.ts +312 -0
  85. package/src/components/data-display/DataViews/data-views-view-mutations.ts +68 -0
  86. package/src/components/data-display/DataViews/index.ts +33 -0
  87. package/src/components/data-display/DataViews/use-data-views-state.ts +349 -0
  88. package/src/components/data-display/DateTimeDisplay/DateTimeDisplay.hooks.ts +104 -0
  89. package/src/components/data-display/DateTimeDisplay/DateTimeDisplay.tsx +68 -118
  90. package/src/components/data-display/DescriptionItem/DescriptionItem.stories.tsx +1 -1
  91. package/src/components/data-display/DescriptionItem/DescriptionItem.tsx +52 -34
  92. package/src/components/data-display/EmptyState/EmptyState.test.stories.tsx +6 -6
  93. package/src/components/data-display/EmptyState/EmptyState.tsx +218 -135
  94. package/src/components/data-display/ErrorState/ErrorState.test.stories.tsx +2 -2
  95. package/src/components/data-display/ErrorState/ErrorState.tsx +83 -55
  96. package/src/components/data-display/HoverCard/HoverCard.stories.tsx +3 -1
  97. package/src/components/data-display/HoverCard/HoverCard.test.stories.tsx +5 -5
  98. package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.hooks.ts +99 -0
  99. package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.styles.ts +87 -0
  100. package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.tsx +28 -155
  101. package/src/components/data-display/LazyImage/LazyImage.hooks.ts +203 -0
  102. package/src/components/data-display/LazyImage/LazyImage.md +1 -1
  103. package/src/components/data-display/LazyImage/LazyImage.stories.tsx +3 -3
  104. package/src/components/data-display/LazyImage/LazyImage.tsx +144 -243
  105. package/src/components/data-display/Lightbox/Lightbox.constants.ts +53 -0
  106. package/src/components/data-display/Lightbox/Lightbox.helpers.ts +39 -0
  107. package/src/components/data-display/Lightbox/Lightbox.hooks.ts +259 -0
  108. package/src/components/data-display/Lightbox/Lightbox.test.stories.tsx +28 -35
  109. package/src/components/data-display/Lightbox/Lightbox.tsx +98 -658
  110. package/src/components/data-display/Lightbox/LightboxControls.tsx +152 -0
  111. package/src/components/data-display/Lightbox/LightboxCurrentMedia.tsx +50 -0
  112. package/src/components/data-display/Lightbox/LightboxMedia.tsx +76 -0
  113. package/src/components/data-display/Lightbox/LightboxOverlay.tsx +83 -0
  114. package/src/components/data-display/Lightbox/LightboxStage.tsx +97 -0
  115. package/src/components/data-display/Lightbox/LightboxThumbnails.tsx +83 -0
  116. package/src/components/data-display/Lightbox/useLightbox.ts +80 -0
  117. package/src/components/data-display/LoadingState/LoadingState.helpers.ts +52 -0
  118. package/src/components/data-display/LoadingState/LoadingState.tsx +117 -87
  119. package/src/components/data-display/MapPreview/MapControlBar.tsx +116 -0
  120. package/src/components/data-display/MapPreview/MapDecorations.tsx +155 -0
  121. package/src/components/data-display/MapPreview/MapLayers.tsx +70 -0
  122. package/src/components/data-display/MapPreview/MapMarkers.tsx +84 -0
  123. package/src/components/data-display/MapPreview/MapOverlays.tsx +68 -0
  124. package/src/components/data-display/MapPreview/MapPreview.constants.ts +19 -0
  125. package/src/components/data-display/MapPreview/MapPreview.helpers.ts +63 -0
  126. package/src/components/data-display/MapPreview/MapPreview.hooks.ts +265 -0
  127. package/src/components/data-display/MapPreview/MapPreview.test.stories.tsx +14 -10
  128. package/src/components/data-display/MapPreview/MapPreview.tsx +52 -882
  129. package/src/components/data-display/MapPreview/MapPreview.types.ts +8 -1
  130. package/src/components/data-display/MapPreview/MapSearchBar.tsx +64 -0
  131. package/src/components/data-display/MapPreview/MapTiles.tsx +70 -0
  132. package/src/components/data-display/MapPreview/StaticMapView.tsx +63 -0
  133. package/src/components/data-display/MapPreview/mapProjection.ts +52 -0
  134. package/src/components/data-display/MapPreview/mapSurface.ts +37 -0
  135. package/src/components/data-display/MapPreview/useMapPreview.ts +62 -0
  136. package/src/components/data-display/Popover/Popover.test.stories.tsx +13 -19
  137. package/src/components/data-display/Progress/Progress.helpers.ts +37 -0
  138. package/src/components/data-display/Progress/Progress.styles.ts +101 -0
  139. package/src/components/data-display/Progress/Progress.tsx +185 -213
  140. package/src/components/data-display/SectionOnboarding/SectionOnboarding.stories.tsx +1 -1
  141. package/src/components/data-display/SectionOnboarding/SectionOnboarding.tsx +1 -1
  142. package/src/components/data-display/StatCard/StatCard.md +117 -0
  143. package/src/components/data-display/StatCard/StatCard.stories.tsx +167 -0
  144. package/src/components/data-display/StatCard/StatCard.test.stories.tsx +240 -0
  145. package/src/components/data-display/StatCard/StatCard.tsx +236 -0
  146. package/src/components/data-display/StatCard/StatCard.types.ts +62 -0
  147. package/src/components/data-display/StatCard/index.ts +7 -0
  148. package/src/components/data-display/Stepper/Stepper.test.stories.tsx +12 -19
  149. package/src/components/data-display/Stepper/Stepper.tsx +1 -1
  150. package/src/components/data-display/Table/Table.helpers.ts +31 -0
  151. package/src/components/data-display/Table/Table.hooks.ts +161 -0
  152. package/src/components/data-display/Table/Table.stories.tsx +3 -1
  153. package/src/components/data-display/Table/Table.styles.ts +223 -0
  154. package/src/components/data-display/Table/Table.test.stories.tsx +4 -4
  155. package/src/components/data-display/Table/Table.tsx +291 -748
  156. package/src/components/data-display/Table/TableParts.hooks.tsx +112 -0
  157. package/src/components/data-display/Table/TableParts.tsx +298 -0
  158. package/src/components/data-display/Table/TableStates.tsx +21 -0
  159. package/src/components/data-display/Timeline/Timeline.stories.tsx +10 -12
  160. package/src/components/data-display/Timeline/Timeline.test.stories.tsx +30 -44
  161. package/src/components/data-display/Timeline/Timeline.tsx +23 -284
  162. package/src/components/data-display/Timeline/TimelineRow.tsx +346 -0
  163. package/src/components/data-display/TimingDiagram/TimingDiagram.tsx +38 -311
  164. package/src/components/data-display/TimingDiagram/TimingDiagram.views.tsx +346 -0
  165. package/src/components/data-display/Tooltip/Tooltip.stories.tsx +4 -1
  166. package/src/components/data-display/Tooltip/Tooltip.test.stories.tsx +44 -58
  167. package/src/components/data-display/Tooltip/Tooltip.tsx +67 -62
  168. package/src/components/data-display/index.ts +1 -0
  169. package/src/components/feedback/ConfirmAction/ConfirmAction.hooks.ts +96 -0
  170. package/src/components/feedback/ConfirmAction/ConfirmAction.md +140 -0
  171. package/src/components/feedback/ConfirmAction/ConfirmAction.parts.tsx +154 -0
  172. package/src/components/feedback/ConfirmAction/ConfirmAction.stories.tsx +146 -0
  173. package/src/components/feedback/ConfirmAction/ConfirmAction.test.stories.tsx +114 -0
  174. package/src/components/feedback/ConfirmAction/ConfirmAction.tsx +46 -0
  175. package/src/components/feedback/ConfirmAction/ConfirmAction.types.ts +111 -0
  176. package/src/components/feedback/ConfirmAction/ConfirmButton.tsx +18 -0
  177. package/src/components/feedback/ConfirmAction/__tests__/confirm-action.test.tsx +288 -0
  178. package/src/components/feedback/ConfirmAction/index.ts +16 -0
  179. package/src/components/feedback/ConfirmAction/with-confirmation.tsx +112 -0
  180. package/src/components/feedback/Dialog/Dialog.styles.ts +4 -2
  181. package/src/components/feedback/Dialog/Dialog.tsx +26 -1
  182. package/src/components/feedback/Dialog/__tests__/dialog-content-spacing.test.tsx +66 -0
  183. package/src/components/feedback/Modal/Modal.md +1 -1
  184. package/src/components/feedback/Modal/Modal.stories.tsx +2 -2
  185. package/src/components/feedback/Sonner/Sonner.tsx +104 -288
  186. package/src/components/feedback/Sonner/Sonner.types.ts +7 -0
  187. package/src/components/feedback/Sonner/SonnerToast.tsx +216 -0
  188. package/src/components/feedback/StackedModal/StackedModal.hooks.ts +226 -0
  189. package/src/components/feedback/StackedModal/StackedModal.parts.tsx +311 -0
  190. package/src/components/feedback/StackedModal/StackedModal.stories.tsx +13 -15
  191. package/src/components/feedback/StackedModal/StackedModal.styles.ts +193 -0
  192. package/src/components/feedback/StackedModal/StackedModal.tsx +100 -619
  193. package/src/components/feedback/StackedModal/StackedModal.types.ts +17 -3
  194. package/src/components/feedback/StackedModal/__tests__/stacked-modal-viewport.test.tsx +104 -0
  195. package/src/components/feedback/StackedModal/modal-stack.tsx +119 -0
  196. package/src/components/feedback/Toast/Toast.tsx +92 -81
  197. package/src/components/feedback/TutorialOverlay/TutorialOverlay.geometry.ts +65 -0
  198. package/src/components/feedback/TutorialOverlay/TutorialOverlay.hooks.ts +185 -0
  199. package/src/components/feedback/TutorialOverlay/TutorialOverlay.test.stories.tsx +2 -2
  200. package/src/components/feedback/TutorialOverlay/TutorialOverlay.tsx +156 -414
  201. package/src/components/feedback/TutorialOverlay/TutorialStepBody.tsx +255 -0
  202. package/src/components/feedback/index.ts +1 -0
  203. package/src/components/form/AddressAutocomplete/AddressAutocomplete.helpers.ts +58 -0
  204. package/src/components/form/AddressAutocomplete/AddressAutocomplete.hooks.ts +93 -0
  205. package/src/components/form/AddressAutocomplete/AddressAutocomplete.mapper.ts +57 -0
  206. package/src/components/form/AddressAutocomplete/AddressAutocomplete.stories.tsx +4 -2
  207. package/src/components/form/AddressAutocomplete/AddressAutocomplete.test.stories.tsx +20 -16
  208. package/src/components/form/AddressAutocomplete/AddressAutocomplete.tsx +26 -829
  209. package/src/components/form/AddressAutocomplete/AddressAutocomplete.types.ts +11 -0
  210. package/src/components/form/AddressAutocomplete/AddressAutocompleteField.tsx +86 -0
  211. package/src/components/form/AddressAutocomplete/AddressFallbackField.tsx +64 -0
  212. package/src/components/form/AddressAutocomplete/AddressInputField.tsx +80 -0
  213. package/src/components/form/AddressAutocomplete/AddressOption.tsx +83 -0
  214. package/src/components/form/AddressAutocomplete/AddressSuggestionsPaper.tsx +24 -0
  215. package/src/components/form/AddressAutocomplete/AddressTextField.tsx +43 -0
  216. package/src/components/form/AddressAutocomplete/mockPlaces.ts +208 -0
  217. package/src/components/form/AddressAutocomplete/useAddressPredictions.ts +136 -0
  218. package/src/components/form/AddressAutocomplete/useAddressSelection.ts +113 -0
  219. package/src/components/form/AddressAutocomplete/useCurrentLocation.ts +116 -0
  220. package/src/components/form/AddressAutocomplete/useGoogleMapsAPI.ts +75 -0
  221. package/src/components/form/Button/Button.helpers.ts +36 -0
  222. package/src/components/form/Button/Button.stories.tsx +4 -1
  223. package/src/components/form/Button/Button.styles.ts +225 -0
  224. package/src/components/form/Button/Button.test.stories.tsx +25 -21
  225. package/src/components/form/Button/Button.tsx +45 -252
  226. package/src/components/form/Calendar/Calendar.dates.ts +100 -0
  227. package/src/components/form/Calendar/Calendar.helpers.ts +32 -0
  228. package/src/components/form/Calendar/Calendar.hooks.ts +278 -0
  229. package/src/components/form/Calendar/Calendar.keyboard.ts +32 -0
  230. package/src/components/form/Calendar/Calendar.selection.ts +156 -0
  231. package/src/components/form/Calendar/Calendar.test.stories.tsx +33 -34
  232. package/src/components/form/Calendar/Calendar.tsx +63 -551
  233. package/src/components/form/Calendar/Calendar.types.ts +16 -0
  234. package/src/components/form/Calendar/CalendarDay.tsx +127 -0
  235. package/src/components/form/Calendar/CalendarDayCell.tsx +52 -0
  236. package/src/components/form/Calendar/CalendarMonth.tsx +128 -0
  237. package/src/components/form/CepField/CepField.md +138 -0
  238. package/src/components/form/CepField/CepField.stories.tsx +138 -0
  239. package/src/components/form/CepField/CepField.test.stories.tsx +281 -0
  240. package/src/components/form/CepField/CepField.tsx +211 -0
  241. package/src/components/form/CepField/CepField.types.ts +73 -0
  242. package/src/components/form/CepField/index.ts +2 -0
  243. package/src/components/form/Checkbox/Checkbox.helpers.ts +28 -0
  244. package/src/components/form/Checkbox/Checkbox.tsx +19 -16
  245. package/src/components/form/CodeEditor/CodeEditor.helpers.ts +45 -0
  246. package/src/components/form/CodeEditor/CodeEditor.hooks.ts +104 -0
  247. package/src/components/form/CodeEditor/CodeEditor.monaco.ts +109 -0
  248. package/src/components/form/CodeEditor/CodeEditor.test.stories.tsx +17 -18
  249. package/src/components/form/CodeEditor/CodeEditor.tsx +81 -330
  250. package/src/components/form/CodeEditor/CodeEditorToolbar.tsx +168 -0
  251. package/src/components/form/Command/Command.hooks.ts +207 -0
  252. package/src/components/form/Command/Command.parts.tsx +58 -0
  253. package/src/components/form/Command/Command.styles.ts +83 -0
  254. package/src/components/form/Command/Command.tsx +61 -270
  255. package/src/components/form/CreatableSelect/CreatableSelect.md +1 -1
  256. package/src/components/form/CreatableSelect/CreatableSelect.test.stories.tsx +1 -1
  257. package/src/components/form/CreatableSelect/CreatableSelect.tsx +152 -71
  258. package/src/components/form/CreatableSelect/CreatableSelect.types.ts +13 -0
  259. package/src/components/form/HeaderButton/HeaderButton.tsx +80 -0
  260. package/src/components/form/HeaderButton/index.ts +2 -0
  261. package/src/components/form/Input/Input.stories.tsx +5 -1
  262. package/src/components/form/Input/Input.styles.ts +176 -0
  263. package/src/components/form/Input/Input.test.stories.tsx +30 -23
  264. package/src/components/form/Input/Input.tsx +71 -211
  265. package/src/components/form/Input/Input.types.ts +11 -0
  266. package/src/components/form/InputOTP/InputOTP.test.stories.tsx +9 -17
  267. package/src/components/form/Menubar/Menubar.test.stories.tsx +4 -8
  268. package/src/components/form/PasswordStrength/PasswordStrength.helpers.tsx +117 -0
  269. package/src/components/form/PasswordStrength/PasswordStrength.test.stories.tsx +3 -3
  270. package/src/components/form/PasswordStrength/PasswordStrength.tsx +202 -255
  271. package/src/components/form/PhoneInput/PhoneInput.hooks.ts +134 -0
  272. package/src/components/form/PhoneInput/PhoneInput.test.stories.tsx +4 -4
  273. package/src/components/form/PhoneInput/PhoneInput.tsx +279 -292
  274. package/src/components/form/PhoneInput/countries.ts +54 -0
  275. package/src/components/form/PhoneInput/phone.ts +47 -0
  276. package/src/components/form/RadioGroup/RadioGroup.test.stories.tsx +9 -9
  277. package/src/components/form/RichTextEditor/RichTextEditor.dom.ts +124 -0
  278. package/src/components/form/RichTextEditor/RichTextEditor.format.ts +61 -0
  279. package/src/components/form/RichTextEditor/RichTextEditor.hooks.ts +142 -0
  280. package/src/components/form/RichTextEditor/RichTextEditor.test.stories.tsx +5 -12
  281. package/src/components/form/RichTextEditor/RichTextEditor.tsx +98 -410
  282. package/src/components/form/RichTextEditor/RichTextEditorToolbar.tsx +187 -0
  283. package/src/components/form/SearchPalette/SearchPalette.parts.tsx +22 -0
  284. package/src/components/form/SearchPalette/SearchPalette.tsx +13 -9
  285. package/src/components/form/Select/Select.test.stories.tsx +35 -27
  286. package/src/components/form/Slider/Slider.test.stories.tsx +9 -11
  287. package/src/components/form/Switch/Switch.md +2 -1
  288. package/src/components/form/Switch/Switch.test.stories.tsx +6 -6
  289. package/src/components/form/Textarea/Textarea.styles.ts +143 -0
  290. package/src/components/form/Textarea/Textarea.test.stories.tsx +20 -26
  291. package/src/components/form/Textarea/Textarea.tsx +114 -535
  292. package/src/components/form/Textarea/TextareaRichEditor.tsx +377 -0
  293. package/src/components/form/Toggle/Toggle.styles.ts +211 -0
  294. package/src/components/form/Toggle/Toggle.test.stories.tsx +19 -27
  295. package/src/components/form/Toggle/Toggle.tsx +10 -185
  296. package/src/components/form/UploadButton/UploadButton.hooks.ts +323 -0
  297. package/src/components/form/UploadButton/UploadButton.parts.tsx +210 -0
  298. package/src/components/form/UploadButton/UploadButton.stories.tsx +2 -1
  299. package/src/components/form/UploadButton/UploadButton.test.stories.tsx +123 -4
  300. package/src/components/form/UploadButton/UploadButton.tsx +76 -305
  301. package/src/components/form/UploadButton/UploadButton.types.ts +16 -0
  302. package/src/components/form/index.ts +1 -0
  303. package/src/components/form/total-form/Fields/CepField.tsx +143 -0
  304. package/src/components/form/total-form/Fields/ToggleField.tsx +61 -0
  305. package/src/components/form/total-form/Fields/index.ts +4 -0
  306. package/src/components/form/total-form/FormContainer.tsx +3 -3
  307. package/src/components/form/total-form/SubmitButton.tsx +25 -2
  308. package/src/components/form/total-form/total-form.test.tsx +118 -1
  309. package/src/components/layout/Accordion/Accordion.md +1 -1
  310. package/src/components/layout/Accordion/Accordion.stories.tsx +5 -1
  311. package/src/components/layout/Accordion/Accordion.styles.ts +80 -0
  312. package/src/components/layout/Accordion/Accordion.test.stories.tsx +32 -50
  313. package/src/components/layout/Accordion/Accordion.tsx +2 -75
  314. package/src/components/layout/Card/Card.helpers.ts +30 -0
  315. package/src/components/layout/Card/Card.stories.tsx +9 -11
  316. package/src/components/layout/Card/Card.styles.ts +182 -0
  317. package/src/components/layout/Card/Card.test.stories.tsx +46 -46
  318. package/src/components/layout/Card/Card.tsx +24 -241
  319. package/src/components/layout/CardGrid/CardGrid.md +96 -0
  320. package/src/components/layout/CardGrid/CardGrid.tsx +140 -0
  321. package/src/components/layout/CardGrid/__tests__/CardGrid.test.tsx +92 -0
  322. package/src/components/layout/CardGrid/index.ts +2 -0
  323. package/src/components/layout/Collapsible/Collapsible.helpers.ts +83 -0
  324. package/src/components/layout/Collapsible/Collapsible.test.stories.tsx +7 -3
  325. package/src/components/layout/Collapsible/Collapsible.tsx +111 -107
  326. package/src/components/layout/Container/Container.test.stories.tsx +11 -16
  327. package/src/components/layout/ContentToolbar/ColumnsMenu.tsx +1 -1
  328. package/src/components/layout/ContentToolbar/ContentToolbar.tsx +120 -44
  329. package/src/components/layout/ContentToolbar/ContentToolbar.types.ts +9 -0
  330. package/src/components/layout/ContentToolbar/FilterTrigger.tsx +1 -1
  331. package/src/components/layout/ContentToolbar/MultiSelectDropdown.tsx +58 -43
  332. package/src/components/layout/ContentToolbar/MultiSelectTriggers.tsx +139 -0
  333. package/src/components/layout/ContentToolbar/SavedViewsMenu.tsx +10 -3
  334. package/src/components/layout/ContentToolbar/SortByDropdown.tsx +35 -11
  335. package/src/components/layout/ContentToolbar/ViewSelector.tsx +5 -6
  336. package/src/components/layout/ContentToolbar/__tests__/MultiSelectDropdown.test.tsx +21 -0
  337. package/src/components/layout/Dashboard/Dashboard.test.stories.tsx +27 -1
  338. package/src/components/layout/Dashboard/Dashboard.tsx +27 -8
  339. package/src/components/layout/Dashboard/DashboardParts.tsx +12 -19
  340. package/src/components/layout/Dashboard/index.ts +2 -1
  341. package/src/components/layout/Drawer/Drawer.stories.tsx +8 -10
  342. package/src/components/layout/Drawer/Drawer.test.stories.tsx +18 -14
  343. package/src/components/layout/Drawer/Drawer.tsx +67 -53
  344. package/src/components/layout/Resizable/Resizable.helpers.ts +133 -0
  345. package/src/components/layout/Resizable/Resizable.hooks.ts +68 -0
  346. package/src/components/layout/Resizable/Resizable.tsx +38 -153
  347. package/src/components/layout/ScrollArea/ScrollArea.helpers.ts +51 -0
  348. package/src/components/layout/ScrollArea/ScrollArea.styles.ts +248 -0
  349. package/src/components/layout/ScrollArea/ScrollArea.tsx +183 -374
  350. package/src/components/layout/ScrollArea/ScrollAreaContent.tsx +70 -0
  351. package/src/components/layout/ScrollArea/ScrollViewport.tsx +90 -0
  352. package/src/components/layout/Separator/Separator.styles.ts +128 -0
  353. package/src/components/layout/Separator/Separator.test.stories.tsx +3 -12
  354. package/src/components/layout/Separator/Separator.tsx +6 -132
  355. package/src/components/layout/SettingsLayout/SettingsLayout.parts.tsx +2 -1
  356. package/src/components/layout/SettingsLayout/SettingsLayout.stories.tsx +6 -8
  357. package/src/components/layout/Sidebar/Sidebar.stories.tsx +10 -12
  358. package/src/components/layout/Sidebar/Sidebar.test.stories.tsx +35 -37
  359. package/src/components/layout/Skeleton/Skeleton.helpers.ts +33 -0
  360. package/src/components/layout/Skeleton/Skeleton.styles.ts +114 -0
  361. package/src/components/layout/Skeleton/Skeleton.test.stories.tsx +3 -3
  362. package/src/components/layout/Skeleton/Skeleton.tsx +67 -155
  363. package/src/components/layout/TableFilter/TableFilter.tsx +2 -1
  364. package/src/components/navigation/Breadcrumbs/BreadcrumbEntry.tsx +305 -0
  365. package/src/components/navigation/Breadcrumbs/Breadcrumbs.md +2 -1
  366. package/src/components/navigation/Breadcrumbs/Breadcrumbs.stories.tsx +12 -14
  367. package/src/components/navigation/Breadcrumbs/Breadcrumbs.styles.tsx +212 -0
  368. package/src/components/navigation/Breadcrumbs/Breadcrumbs.test.stories.tsx +62 -61
  369. package/src/components/navigation/Breadcrumbs/Breadcrumbs.tsx +116 -429
  370. package/src/components/navigation/CommandPalette/CommandPalette.helpers.ts +82 -0
  371. package/src/components/navigation/CommandPalette/CommandPalette.hooks.ts +202 -0
  372. package/src/components/navigation/CommandPalette/CommandPalette.stories.tsx +3 -1
  373. package/src/components/navigation/CommandPalette/CommandPalette.test.stories.tsx +3 -5
  374. package/src/components/navigation/CommandPalette/CommandPalette.tsx +108 -341
  375. package/src/components/navigation/CommandPalette/CommandPaletteResults.tsx +182 -0
  376. package/src/components/navigation/CommandPalette/ShortcutChip.tsx +18 -0
  377. package/src/components/navigation/ContextMenu/ContextMenu.md +3 -1
  378. package/src/components/navigation/ContextMenu/ContextMenu.stories.tsx +4 -1
  379. package/src/components/navigation/ContextMenu/ContextMenu.test.stories.tsx +19 -24
  380. package/src/components/navigation/ContextMenu/ContextMenu.tsx +70 -54
  381. package/src/components/navigation/DropdownMenu/DropdownMenu.md +3 -1
  382. package/src/components/navigation/DropdownMenu/DropdownMenu.stories.tsx +17 -19
  383. package/src/components/navigation/DropdownMenu/DropdownMenu.test.stories.tsx +26 -26
  384. package/src/components/navigation/DropdownMenu/DropdownMenu.tsx +189 -111
  385. package/src/components/navigation/NavigationMenu/NavigationMegaSection.tsx +66 -0
  386. package/src/components/navigation/NavigationMenu/NavigationMenu.md +7 -2
  387. package/src/components/navigation/NavigationMenu/NavigationMenu.shell.tsx +218 -0
  388. package/src/components/navigation/NavigationMenu/NavigationMenu.styles.ts +223 -0
  389. package/src/components/navigation/NavigationMenu/NavigationMenu.tsx +119 -808
  390. package/src/components/navigation/NavigationMenu/NavigationMenuItem.hooks.ts +126 -0
  391. package/src/components/navigation/NavigationMenu/NavigationMenuItem.tsx +352 -0
  392. package/src/components/navigation/NavigationMenu/NavigationStandardBody.tsx +148 -0
  393. package/src/components/navigation/NavigationMenu/stories/NavigationMenu.stories.tsx +15 -17
  394. package/src/components/navigation/NavigationMenu/stories/NavigationMenu.test.stories.tsx +27 -23
  395. package/src/components/navigation/NavigationMenu/stories/NavigationMenuHorizontal.stories.tsx +11 -13
  396. package/src/components/navigation/NavigationMenu/stories/NavigationMenuVertical.stories.tsx +9 -11
  397. package/src/components/navigation/Pagination/Pagination.helpers.tsx +85 -0
  398. package/src/components/navigation/Pagination/Pagination.md +2 -1
  399. package/src/components/navigation/Pagination/Pagination.stories.tsx +4 -6
  400. package/src/components/navigation/Pagination/Pagination.styles.ts +135 -0
  401. package/src/components/navigation/Pagination/Pagination.tsx +178 -313
  402. package/src/components/navigation/Pagination/PaginationParts.tsx +81 -0
  403. package/src/components/navigation/Tabs/Tabs.md +3 -1
  404. package/src/components/navigation/Tabs/Tabs.stories.tsx +21 -23
  405. package/src/components/navigation/Tabs/Tabs.styles.ts +163 -0
  406. package/src/components/navigation/Tabs/Tabs.test.stories.tsx +37 -37
  407. package/src/components/navigation/Tabs/Tabs.tsx +192 -361
  408. package/src/components/navigation/Tabs/TabsPanel.tsx +100 -0
  409. package/src/components/typography/Blockquote/Blockquote.tsx +1 -1
  410. package/src/components/typography/Code/Code.test.stories.tsx +5 -5
  411. package/src/components/typography/Code/Code.tsx +74 -49
  412. package/src/components/typography/Heading/Heading.test.stories.tsx +8 -8
  413. package/src/components/typography/Paragraph/Paragraph.test.stories.tsx +5 -12
  414. package/src/components/typography/Text/Text.tsx +84 -77
  415. package/src/components/utility/AnimatedIcon/AnimatedIcon.md +4 -4
  416. package/src/components/utility/AnimatedIcon/AnimatedIcon.stories.tsx +19 -21
  417. package/src/components/utility/AnimatedIcon/AnimatedIcon.test.stories.tsx +19 -12
  418. package/src/components/utility/AspectRatio/AspectRatio.test.stories.tsx +9 -9
  419. package/src/components/utility/InfiniteScroll/InfiniteScroll.hooks.ts +118 -0
  420. package/src/components/utility/InfiniteScroll/InfiniteScroll.parts.tsx +54 -0
  421. package/src/components/utility/InfiniteScroll/InfiniteScroll.test.stories.tsx +12 -9
  422. package/src/components/utility/InfiniteScroll/InfiniteScroll.tsx +29 -148
  423. package/src/components/utility/InstallPrompt/InstallPrompt.helpers.ts +124 -0
  424. package/src/components/utility/InstallPrompt/InstallPrompt.md +113 -0
  425. package/src/components/utility/InstallPrompt/InstallPrompt.stories.tsx +118 -0
  426. package/src/components/utility/InstallPrompt/InstallPrompt.styles.ts +50 -0
  427. package/src/components/utility/InstallPrompt/InstallPrompt.test.stories.tsx +83 -0
  428. package/src/components/utility/InstallPrompt/InstallPrompt.tsx +192 -0
  429. package/src/components/utility/InstallPrompt/InstallPrompt.types.ts +78 -0
  430. package/src/components/utility/InstallPrompt/__tests__/InstallPrompt.ios.test.tsx +65 -0
  431. package/src/components/utility/InstallPrompt/__tests__/usePwaInstall.test.tsx +271 -0
  432. package/src/components/utility/InstallPrompt/index.ts +10 -0
  433. package/src/components/utility/InstallPrompt/usePwaInstall.ts +171 -0
  434. package/src/components/utility/Transition/Transition.helpers.ts +40 -0
  435. package/src/components/utility/Transition/Transition.tsx +17 -68
  436. package/src/components/utility/VirtualList/VirtualList.hooks.ts +319 -0
  437. package/src/components/utility/VirtualList/VirtualList.test.stories.tsx +49 -38
  438. package/src/components/utility/VirtualList/VirtualList.tsx +110 -315
  439. package/src/components/utility/WorkflowStep/WorkflowStep.helpers.ts +151 -0
  440. package/src/components/utility/WorkflowStep/WorkflowStep.parts.tsx +371 -0
  441. package/src/components/utility/WorkflowStep/WorkflowStep.stories.tsx +9 -11
  442. package/src/components/utility/WorkflowStep/WorkflowStep.test.stories.tsx +7 -7
  443. package/src/components/utility/WorkflowStep/WorkflowStep.tsx +51 -449
  444. package/src/components/utility/WorkflowStep/WorkflowStepRow.tsx +96 -0
  445. package/src/components/utility/index.ts +1 -0
  446. package/src/mui/Link.ts +2 -0
  447. package/src/utils/index.ts +2 -1
  448. 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
+ };