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