@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,65 @@
1
+ /**
2
+ * `InstallPrompt` on iOS Safari.
3
+ *
4
+ * Its own file because the platform probe has to be replaced at module scope,
5
+ * and `vi.mock` is hoisted over the whole file — the Chromium cases in
6
+ * `usePwaInstall.test.tsx` must keep the real probe.
7
+ *
8
+ * What matters here is the ABSENCE of the install button. Safari exposes no way
9
+ * to open its own Add to Home Screen sheet, so a button on iOS would be a
10
+ * control that does nothing when tapped; the instructions take its place.
11
+ */
12
+ import { render, screen, waitFor } from '@testing-library/react';
13
+ import { afterEach, describe, expect, it, vi } from 'vitest';
14
+
15
+ import { InstallPrompt } from '../index';
16
+
17
+ vi.mock('../InstallPrompt.helpers', async (importOriginal) => {
18
+ const actual = await importOriginal<typeof import('../InstallPrompt.helpers')>();
19
+
20
+ return { ...actual, resolveInstallPlatform: () => 'ios' as const };
21
+ });
22
+
23
+ describe('InstallPrompt on iOS Safari', () => {
24
+ afterEach(() => {
25
+ window.localStorage.clear();
26
+ });
27
+
28
+ it('shows without waiting for an event that iOS never fires', async () => {
29
+ render(<InstallPrompt title="Instalar FutureDrink" />);
30
+
31
+ await waitFor(() => {
32
+ expect(screen.getByTestId('install-prompt')).toBeInTheDocument();
33
+ });
34
+ expect(screen.getByTestId('install-prompt-title')).toHaveTextContent('Instalar FutureDrink');
35
+ });
36
+
37
+ it('offers instructions instead of an install button', async () => {
38
+ render(<InstallPrompt />);
39
+
40
+ await waitFor(() => {
41
+ expect(screen.getByTestId('install-prompt-ios-instructions')).toBeInTheDocument();
42
+ });
43
+ await waitFor(() => {
44
+ expect(screen.queryByTestId('install-prompt-install')).not.toBeInTheDocument();
45
+ });
46
+ });
47
+
48
+ it('accepts host-supplied instruction copy', async () => {
49
+ render(<InstallPrompt iosInstructions={<span>Toque em Compartilhar</span>} />);
50
+
51
+ await waitFor(() => {
52
+ expect(screen.getByTestId('install-prompt-ios-instructions')).toHaveTextContent(
53
+ 'Toque em Compartilhar',
54
+ );
55
+ });
56
+ });
57
+
58
+ it('can still be dismissed', async () => {
59
+ render(<InstallPrompt />);
60
+
61
+ await waitFor(() => {
62
+ expect(screen.getByTestId('install-prompt-dismiss')).toBeInTheDocument();
63
+ });
64
+ });
65
+ });
@@ -0,0 +1,271 @@
1
+ /**
2
+ * `usePwaInstall` / `InstallPrompt` — the machinery that turns an installable
3
+ * site into one that actually asks.
4
+ *
5
+ * The regression this file exists for: a manifest and a service worker satisfy
6
+ * every installability criterion a browser checks, and still produce no prompt
7
+ * of any kind, because nothing in the page captures `beforeinstallprompt`. The
8
+ * `preventDefault` case below guards that hole — without that call the browser
9
+ * reclaims the event and the app can never install itself.
10
+ *
11
+ * The iOS branch lives in `InstallPrompt.ios.test.tsx`, which needs a
12
+ * module-level mock this file must not carry.
13
+ */
14
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
15
+ import { afterEach, describe, expect, it, vi } from 'vitest';
16
+
17
+ import { InstallPrompt } from '../index';
18
+ import {
19
+ isDismissalActive,
20
+ isIosSafariAgent,
21
+ readDismissedAt,
22
+ writeDismissedAt,
23
+ } from '../InstallPrompt.helpers';
24
+ import type { BeforeInstallPromptEvent } from '../InstallPrompt.types';
25
+ import { usePwaInstall } from '../usePwaInstall';
26
+
27
+ const STORAGE_KEY = 'pwa-install-dismissed';
28
+
29
+ // A fixed clock. Deriving expectations from the real `Date.now()` would make
30
+ // them depend on how long the test took to run.
31
+ const NOW = 1_700_000_000_000;
32
+ const DAY = 86_400_000;
33
+
34
+ type MockPromptEvent = BeforeInstallPromptEvent & { prompt: ReturnType<typeof vi.fn> };
35
+
36
+ /**
37
+ * Stands in for the Chromium event: `prompt()` records that it was called and
38
+ * `userChoice` resolves with whatever answer the test wants the user to give.
39
+ */
40
+ const createPromptEvent = (outcome: 'accepted' | 'dismissed'): MockPromptEvent => {
41
+ const event = new Event('beforeinstallprompt') as MockPromptEvent;
42
+
43
+ Object.assign(event, {
44
+ platforms: ['web'],
45
+ prompt: vi.fn().mockResolvedValue(undefined),
46
+ userChoice: Promise.resolve({ outcome, platform: 'web' }),
47
+ });
48
+
49
+ return event;
50
+ };
51
+
52
+ /** Renders the hook's state into the DOM so assertions can read it directly. */
53
+ const HookProbe = ({ dismissForDays }: { dismissForDays?: number }) => {
54
+ const { canInstall, platform, isInstalled } = usePwaInstall({ dismissForDays });
55
+
56
+ return (
57
+ <div>
58
+ <span data-testid="can-install">{String(canInstall)}</span>
59
+ <span data-testid="platform">{platform}</span>
60
+ <span data-testid="is-installed">{String(isInstalled)}</span>
61
+ </div>
62
+ );
63
+ };
64
+
65
+ const IPHONE_SAFARI =
66
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1';
67
+ const IPADOS_SAFARI =
68
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15';
69
+ const CHROME_ON_IOS =
70
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/120.0 Mobile/15E148 Safari/604.1';
71
+ const DESKTOP_CHROME =
72
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
73
+
74
+ describe('isIosSafariAgent', () => {
75
+ it('detects Safari on iPhone', () => {
76
+ expect(isIosSafariAgent(IPHONE_SAFARI, 5)).toBe(true);
77
+ });
78
+
79
+ it('detects iPadOS, which reports a Macintosh agent with touch points', () => {
80
+ expect(isIosSafariAgent(IPADOS_SAFARI, 5)).toBe(true);
81
+ });
82
+
83
+ it('rejects desktop Safari, which shares that agent but has no touch points', () => {
84
+ expect(isIosSafariAgent(IPADOS_SAFARI, 0)).toBe(false);
85
+ });
86
+
87
+ it('rejects Chrome on iOS, which cannot add to the home screen at all', () => {
88
+ expect(isIosSafariAgent(CHROME_ON_IOS, 5)).toBe(false);
89
+ });
90
+
91
+ it('rejects desktop Chrome', () => {
92
+ expect(isIosSafariAgent(DESKTOP_CHROME, 0)).toBe(false);
93
+ });
94
+ });
95
+
96
+ describe('InstallPrompt dismissal storage', () => {
97
+ afterEach(() => {
98
+ window.localStorage.clear();
99
+ });
100
+
101
+ it('reports no dismissal when nothing was ever stored', () => {
102
+ expect(readDismissedAt(STORAGE_KEY)).toBeNull();
103
+ expect(isDismissalActive(null, 30, NOW)).toBe(false);
104
+ });
105
+
106
+ it('round-trips a dismissal timestamp', () => {
107
+ writeDismissedAt(STORAGE_KEY, NOW);
108
+ expect(readDismissedAt(STORAGE_KEY)).toBe(NOW);
109
+ });
110
+
111
+ it('ignores a corrupted stored value instead of throwing', () => {
112
+ window.localStorage.setItem(STORAGE_KEY, 'not-a-number');
113
+ expect(readDismissedAt(STORAGE_KEY)).toBeNull();
114
+ });
115
+
116
+ it('keeps a dismissal active inside the window and expires it after', () => {
117
+ expect(isDismissalActive(NOW, 30, NOW + 29 * DAY)).toBe(true);
118
+ expect(isDismissalActive(NOW, 30, NOW + 31 * DAY)).toBe(false);
119
+ });
120
+
121
+ it('treats dismissForDays of 0 as permanent', () => {
122
+ expect(isDismissalActive(NOW, 0, NOW + 3650 * DAY)).toBe(true);
123
+ });
124
+
125
+ it('treats a future timestamp as active so a wound-back clock cannot re-prompt', () => {
126
+ expect(isDismissalActive(NOW + DAY, 30, NOW)).toBe(true);
127
+ });
128
+ });
129
+
130
+ describe('usePwaInstall', () => {
131
+ afterEach(() => {
132
+ window.localStorage.clear();
133
+ });
134
+
135
+ it('cannot install before the browser offers a prompt', async () => {
136
+ render(<HookProbe />);
137
+
138
+ await waitFor(() => {
139
+ expect(screen.getByTestId('platform')).toHaveTextContent('unsupported');
140
+ });
141
+ expect(screen.getByTestId('can-install')).toHaveTextContent('false');
142
+ });
143
+
144
+ it('becomes installable once beforeinstallprompt fires', async () => {
145
+ render(<HookProbe />);
146
+ fireEvent(window, createPromptEvent('accepted'));
147
+
148
+ await waitFor(() => {
149
+ expect(screen.getByTestId('can-install')).toHaveTextContent('true');
150
+ });
151
+ expect(screen.getByTestId('platform')).toHaveTextContent('prompt');
152
+ });
153
+
154
+ it('calls preventDefault so the deferred handle survives', async () => {
155
+ render(<HookProbe />);
156
+
157
+ const event = createPromptEvent('accepted');
158
+ const preventDefault = vi.spyOn(event, 'preventDefault');
159
+ fireEvent(window, event);
160
+
161
+ await waitFor(() => {
162
+ expect(preventDefault).toHaveBeenCalled();
163
+ });
164
+ });
165
+
166
+ // `dismissForDays: 0` makes the stored dismissal permanent, so this asserts
167
+ // that the hook consults storage at all without depending on the wall clock.
168
+ // The expiry arithmetic itself is covered above against a fixed clock.
169
+ it('stays suppressed while a dismissal is stored', async () => {
170
+ writeDismissedAt(STORAGE_KEY, NOW);
171
+
172
+ render(<HookProbe dismissForDays={0} />);
173
+ fireEvent(window, createPromptEvent('accepted'));
174
+
175
+ await waitFor(() => {
176
+ expect(screen.getByTestId('platform')).toHaveTextContent('prompt');
177
+ });
178
+ expect(screen.getByTestId('can-install')).toHaveTextContent('false');
179
+ });
180
+
181
+ it('reports installed and stays suppressed after appinstalled', async () => {
182
+ render(<HookProbe />);
183
+ fireEvent(window, createPromptEvent('accepted'));
184
+
185
+ await waitFor(() => {
186
+ expect(screen.getByTestId('can-install')).toHaveTextContent('true');
187
+ });
188
+
189
+ fireEvent(window, new Event('appinstalled'));
190
+
191
+ await waitFor(() => {
192
+ expect(screen.getByTestId('is-installed')).toHaveTextContent('true');
193
+ });
194
+ expect(screen.getByTestId('can-install')).toHaveTextContent('false');
195
+ });
196
+ });
197
+
198
+ describe('InstallPrompt', () => {
199
+ afterEach(() => {
200
+ window.localStorage.clear();
201
+ });
202
+
203
+ it('renders nothing until the browser offers a prompt', async () => {
204
+ render(<InstallPrompt />);
205
+
206
+ await waitFor(() => {
207
+ expect(screen.queryByTestId('install-prompt')).not.toBeInTheDocument();
208
+ });
209
+ });
210
+
211
+ it('appears with an install button once the prompt is captured', async () => {
212
+ render(<InstallPrompt title="Install FutureDrink" />);
213
+ fireEvent(window, createPromptEvent('accepted'));
214
+
215
+ await waitFor(() => {
216
+ expect(screen.getByTestId('install-prompt')).toBeInTheDocument();
217
+ });
218
+ expect(screen.getByTestId('install-prompt-install')).toBeInTheDocument();
219
+ });
220
+
221
+ it('fires the native prompt and reports the accepted outcome', async () => {
222
+ const onInstall = vi.fn();
223
+
224
+ render(<InstallPrompt onInstall={onInstall} />);
225
+ const event = createPromptEvent('accepted');
226
+ fireEvent(window, event);
227
+
228
+ await waitFor(() => {
229
+ expect(screen.getByTestId('install-prompt-install')).toBeInTheDocument();
230
+ });
231
+ fireEvent.click(screen.getByTestId('install-prompt-install'));
232
+
233
+ await waitFor(() => {
234
+ expect(onInstall).toHaveBeenCalledWith('accepted');
235
+ });
236
+ expect(event.prompt).toHaveBeenCalledTimes(1);
237
+ });
238
+
239
+ it('records a dismissal when the user declines the native dialog', async () => {
240
+ render(<InstallPrompt onInstall={vi.fn()} />);
241
+ fireEvent(window, createPromptEvent('dismissed'));
242
+
243
+ await waitFor(() => {
244
+ expect(screen.getByTestId('install-prompt-install')).toBeInTheDocument();
245
+ });
246
+ fireEvent.click(screen.getByTestId('install-prompt-install'));
247
+
248
+ await waitFor(() => {
249
+ expect(screen.queryByTestId('install-prompt')).not.toBeInTheDocument();
250
+ });
251
+ expect(readDismissedAt(STORAGE_KEY)).not.toBeNull();
252
+ });
253
+
254
+ it('hides and persists the dismissal when the close button is used', async () => {
255
+ const onDismiss = vi.fn();
256
+
257
+ render(<InstallPrompt onDismiss={onDismiss} />);
258
+ fireEvent(window, createPromptEvent('accepted'));
259
+
260
+ await waitFor(() => {
261
+ expect(screen.getByTestId('install-prompt-dismiss')).toBeInTheDocument();
262
+ });
263
+ fireEvent.click(screen.getByTestId('install-prompt-dismiss'));
264
+
265
+ await waitFor(() => {
266
+ expect(screen.queryByTestId('install-prompt')).not.toBeInTheDocument();
267
+ });
268
+ expect(onDismiss).toHaveBeenCalledTimes(1);
269
+ expect(readDismissedAt(STORAGE_KEY)).not.toBeNull();
270
+ });
271
+ });
@@ -0,0 +1,10 @@
1
+ export { InstallPrompt } from './InstallPrompt';
2
+ export { usePwaInstall } from './usePwaInstall';
3
+ export type {
4
+ BeforeInstallPromptEvent,
5
+ InstallOutcome,
6
+ InstallPlatform,
7
+ InstallPromptProps,
8
+ PwaInstallState,
9
+ UsePwaInstallOptions,
10
+ } from './InstallPrompt.types';
@@ -0,0 +1,171 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+
3
+ import {
4
+ DEFAULT_DISMISS_DAYS,
5
+ DEFAULT_STORAGE_KEY,
6
+ isDismissalActive,
7
+ isStandaloneDisplay,
8
+ readDismissedAt,
9
+ resolveInstallPlatform,
10
+ writeDismissedAt,
11
+ } from './InstallPrompt.helpers';
12
+ import type {
13
+ BeforeInstallPromptEvent,
14
+ InstallOutcome,
15
+ InstallPlatform,
16
+ PwaInstallState,
17
+ UsePwaInstallOptions,
18
+ } from './InstallPrompt.types';
19
+
20
+ /**
21
+ * Headless PWA install machinery.
22
+ *
23
+ * A manifest and a service worker make a site installABLE; they do not make it
24
+ * install. Chromium announces its willingness by firing `beforeinstallprompt`
25
+ * and then does nothing further on its own beyond a small address-bar icon most
26
+ * users never notice. Unless a page calls `preventDefault()` on that event and
27
+ * keeps it, the handle is gone and the app can never ask.
28
+ *
29
+ * That is the whole reason this hook exists. It captures the event, tracks the
30
+ * three states that should suppress an install invitation (already installed,
31
+ * recently dismissed, browser cannot install), and hands back a
32
+ * gesture-callable `promptInstall`.
33
+ *
34
+ * Every environment probe runs in an effect rather than in a state initialiser:
35
+ * touching `window` during render breaks SSR and desynchronises hydration, so
36
+ * the first paint deliberately reports `canInstall: false` and corrects itself
37
+ * on mount.
38
+ */
39
+
40
+ /** Tracks whether the page is running as an installed app. */
41
+ const useInstalledState = (onInstalled?: () => void) => {
42
+ const [isInstalled, setIsInstalled] = useState(false);
43
+
44
+ // Held in a ref, and refreshed in its own effect rather than during render, so
45
+ // that a caller passing an inline callback does not re-subscribe the listener
46
+ // on every render.
47
+ const onInstalledRef = useRef(onInstalled);
48
+ useEffect(() => {
49
+ onInstalledRef.current = onInstalled;
50
+ }, [onInstalled]);
51
+
52
+ useEffect(() => {
53
+ setIsInstalled(isStandaloneDisplay());
54
+
55
+ const markInstalled = () => {
56
+ setIsInstalled(true);
57
+ onInstalledRef.current?.();
58
+ };
59
+
60
+ window.addEventListener('appinstalled', markInstalled);
61
+ return () => window.removeEventListener('appinstalled', markInstalled);
62
+ }, []);
63
+
64
+ return isInstalled;
65
+ };
66
+
67
+ /**
68
+ * Captures and holds the deferred `beforeinstallprompt` event.
69
+ *
70
+ * The listener is attached on mount, which is normally AFTER the browser would
71
+ * have fired the event on a cold load. Chromium re-fires it on navigation and
72
+ * on re-evaluation of the install criteria, so a missed first event resolves
73
+ * itself; a host that needs the very first one can attach an early capture in
74
+ * its HTML entry point and re-dispatch it.
75
+ */
76
+ const useDeferredPrompt = () => {
77
+ const deferredRef = useRef<BeforeInstallPromptEvent | null>(null);
78
+ const [promptReady, setPromptReady] = useState(false);
79
+
80
+ useEffect(() => {
81
+ const capture = (event: Event) => {
82
+ // Without preventDefault the browser owns the event and the handle dies
83
+ // with it — this is the line that makes an in-page install button possible.
84
+ event.preventDefault();
85
+ deferredRef.current = event as BeforeInstallPromptEvent;
86
+ setPromptReady(true);
87
+ };
88
+
89
+ window.addEventListener('beforeinstallprompt', capture);
90
+ return () => window.removeEventListener('beforeinstallprompt', capture);
91
+ }, []);
92
+
93
+ const clearPrompt = useCallback(() => {
94
+ deferredRef.current = null;
95
+ setPromptReady(false);
96
+ }, []);
97
+
98
+ return { deferredRef, promptReady, clearPrompt };
99
+ };
100
+
101
+ /** Reads and writes the "don't ask again for a while" record. */
102
+ const useDismissal = (storageKey: string, dismissForDays: number) => {
103
+ const [dismissed, setDismissed] = useState(false);
104
+
105
+ useEffect(() => {
106
+ const dismissedAt = readDismissedAt(storageKey);
107
+ setDismissed(isDismissalActive(dismissedAt, dismissForDays, Date.now()));
108
+ }, [storageKey, dismissForDays]);
109
+
110
+ const persistDismissal = useCallback(() => {
111
+ writeDismissedAt(storageKey, Date.now());
112
+ setDismissed(true);
113
+ }, [storageKey]);
114
+
115
+ return { dismissed, persistDismissal };
116
+ };
117
+
118
+ /** Resolves the install route once the deferred-prompt state is known. */
119
+ const useInstallPlatform = (promptReady: boolean): InstallPlatform => {
120
+ const [platform, setPlatform] = useState<InstallPlatform>('unsupported');
121
+
122
+ useEffect(() => {
123
+ setPlatform(resolveInstallPlatform(promptReady));
124
+ }, [promptReady]);
125
+
126
+ return platform;
127
+ };
128
+
129
+ export const usePwaInstall = (options: UsePwaInstallOptions = {}): PwaInstallState => {
130
+ const {
131
+ storageKey = DEFAULT_STORAGE_KEY,
132
+ dismissForDays = DEFAULT_DISMISS_DAYS,
133
+ onInstalled,
134
+ } = options;
135
+
136
+ const isInstalled = useInstalledState(onInstalled);
137
+ const { deferredRef, promptReady, clearPrompt } = useDeferredPrompt();
138
+ const { dismissed, persistDismissal } = useDismissal(storageKey, dismissForDays);
139
+ const platform = useInstallPlatform(promptReady);
140
+
141
+ const promptInstall = useCallback(async (): Promise<InstallOutcome> => {
142
+ const deferred = deferredRef.current;
143
+ if (!deferred) return 'unavailable';
144
+
145
+ await deferred.prompt();
146
+ const { outcome } = await deferred.userChoice;
147
+
148
+ // Single-use handle: spent whichever way the user answered. Chromium emits a
149
+ // fresh event if the app is still installable, so holding this one would
150
+ // only mean calling prompt() twice on a dead event.
151
+ clearPrompt();
152
+
153
+ // Declining the browser's own dialog is a real "no" — record it, or the
154
+ // affordance reappears on the next render and badgers the user.
155
+ if (outcome === 'dismissed') persistDismissal();
156
+
157
+ return outcome;
158
+ }, [deferredRef, clearPrompt, persistDismissal]);
159
+
160
+ // iOS has no event to wait for, so its affordance is gated on the platform
161
+ // probe alone; everywhere else a live captured prompt is the prerequisite.
162
+ const canInstall = !isInstalled && !dismissed && (platform === 'ios' || promptReady);
163
+
164
+ return {
165
+ canInstall,
166
+ platform,
167
+ isInstalled,
168
+ promptInstall,
169
+ dismiss: persistDismissal,
170
+ };
171
+ };
@@ -0,0 +1,40 @@
1
+ import type { Theme } from '@mui/material';
2
+
3
+ import type { CustomTransitionProps, TransitionVariant } from './Transition.types';
4
+
5
+ type Duration = NonNullable<CustomTransitionProps['duration']>;
6
+ type Easing = NonNullable<CustomTransitionProps['easing']>;
7
+
8
+ /** An explicit `duration` wins in either form; otherwise the variant picks one. */
9
+ export const transitionDuration = (
10
+ theme: Theme,
11
+ variant: TransitionVariant,
12
+ duration?: Duration,
13
+ ): Duration => {
14
+ if (typeof duration === 'number' || typeof duration === 'object') return duration;
15
+
16
+ switch (variant) {
17
+ case 'slide':
18
+ return theme.transitions.duration.enteringScreen;
19
+ case 'fade':
20
+ case 'scale':
21
+ case 'grow':
22
+ case 'zoom':
23
+ return theme.transitions.duration.shorter;
24
+ default:
25
+ return theme.transitions.duration.standard;
26
+ }
27
+ };
28
+
29
+ /** As above: an explicit `easing` wins, otherwise the variant picks one. */
30
+ export const transitionEasing = (
31
+ theme: Theme,
32
+ variant: TransitionVariant,
33
+ easing?: Easing,
34
+ ): Easing => {
35
+ if (typeof easing === 'string' || typeof easing === 'object') return easing;
36
+
37
+ return variant === 'slide'
38
+ ? theme.transitions.easing.easeOut
39
+ : theme.transitions.easing.easeInOut;
40
+ };