@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
@@ -1,4 +1,7 @@
1
- import { Favorite, Refresh,Settings, Star } from '@mui/icons-material';
1
+ import Favorite from '@mui/icons-material/Favorite';
2
+ import Refresh from '@mui/icons-material/Refresh';
3
+ import Settings from '@mui/icons-material/Settings';
4
+ import Star from '@mui/icons-material/Star';
2
5
  import { Box, Stack, Typography } from '@mui/material';
3
6
  import type { Meta, StoryObj } from '@storybook/react-vite';
4
7
  import React from 'react';
@@ -160,17 +163,18 @@ export const KeyboardNavigation: Story = {
160
163
  const secondIcon = canvas.getByLabelText('Second animated icon');
161
164
  const thirdIcon = canvas.getByLabelText('Third animated icon');
162
165
 
163
- // Focus first icon
164
- firstIcon.focus();
165
- await expect(firstIcon).toHaveFocus();
166
+ // Tab to the first icon rather than focusing it directly, so the whole
167
+ // sequence below exercises one keyboard journey through the three icons.
168
+ await userEvent.tab();
169
+ await waitFor(() => expect(firstIcon).toHaveFocus());
166
170
 
167
171
  // Tab to second icon
168
172
  await userEvent.tab();
169
- await expect(secondIcon).toHaveFocus();
173
+ await waitFor(() => expect(secondIcon).toHaveFocus());
170
174
 
171
175
  // Tab to third icon
172
176
  await userEvent.tab();
173
- await expect(thirdIcon).toHaveFocus();
177
+ await waitFor(() => expect(thirdIcon).toHaveFocus());
174
178
 
175
179
  // Test Enter key activation
176
180
  await userEvent.keyboard('{Enter}');
@@ -276,13 +280,16 @@ export const FocusManagement: Story = {
276
280
  const secondIcon = canvas.getByLabelText('Focus test icon 2');
277
281
 
278
282
  // Focus first icon
279
- firstIcon.focus();
280
- await expect(firstIcon).toHaveFocus();
283
+ await userEvent.tab();
284
+ await waitFor(() => expect(firstIcon).toHaveFocus());
281
285
 
282
- // Move focus to second icon
283
- secondIcon.focus();
284
- await expect(secondIcon).toHaveFocus();
285
- await expect(firstIcon).not.toHaveFocus();
286
+ // Move focus to second icon — asserted together so the hand-off is checked
287
+ // as one state, not as two reads that could straddle a re-render.
288
+ await userEvent.tab();
289
+ await waitFor(() => {
290
+ expect(secondIcon).toHaveFocus();
291
+ expect(firstIcon).not.toHaveFocus();
292
+ });
286
293
  },
287
294
  };
288
295
 
@@ -1,6 +1,6 @@
1
1
  import { Box, Typography } from '@mui/material';
2
2
  import type { Meta, StoryObj } from '@storybook/react-vite';
3
- import { expect, userEvent, waitFor,within } from 'storybook/test';
3
+ import { expect, userEvent, waitFor, within } from 'storybook/test';
4
4
 
5
5
  import { AspectRatio } from './AspectRatio';
6
6
 
@@ -198,8 +198,8 @@ export const KeyboardNavigation: Story = {
198
198
  const component = canvas.getByTestId('keyboard-aspect-ratio');
199
199
 
200
200
  // Focus the component
201
- component.focus();
202
- await expect(component).toHaveFocus();
201
+ await userEvent.click(component);
202
+ await waitFor(() => expect(component).toHaveFocus());
203
203
  });
204
204
 
205
205
  await step('Tab navigation', async () => {
@@ -207,7 +207,7 @@ export const KeyboardNavigation: Story = {
207
207
 
208
208
  // Tab to next element
209
209
  await userEvent.tab();
210
- await expect(nextButton).toHaveFocus();
210
+ await waitFor(() => expect(nextButton).toHaveFocus());
211
211
  });
212
212
  },
213
213
  };
@@ -294,20 +294,20 @@ export const FocusManagement: Story = {
294
294
  const afterButton = canvas.getByTestId('after-button');
295
295
 
296
296
  // Start with first button
297
- beforeButton.focus();
298
- await expect(beforeButton).toHaveFocus();
297
+ await userEvent.click(beforeButton);
298
+ await waitFor(() => expect(beforeButton).toHaveFocus());
299
299
 
300
300
  // Tab to aspect ratio container
301
301
  await userEvent.tab();
302
- await expect(aspectRatio).toHaveFocus();
302
+ await waitFor(() => expect(aspectRatio).toHaveFocus());
303
303
 
304
304
  // Tab to focusable content inside
305
305
  await userEvent.tab();
306
- await expect(focusableContent).toHaveFocus();
306
+ await waitFor(() => expect(focusableContent).toHaveFocus());
307
307
 
308
308
  // Tab to after button
309
309
  await userEvent.tab();
310
- await expect(afterButton).toHaveFocus();
310
+ await waitFor(() => expect(afterButton).toHaveFocus());
311
311
  });
312
312
  },
313
313
  };
@@ -0,0 +1,118 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+
3
+ import type { InfiniteScrollProps } from './InfiniteScroll.types';
4
+
5
+ type ScrollableTarget = InfiniteScrollProps['scrollableTarget'];
6
+
7
+ /** A string target is looked up by id first, then as a selector. */
8
+ const resolveScrollable = (scrollableTarget: ScrollableTarget): Element | null => {
9
+ if (!scrollableTarget) return null;
10
+
11
+ if (typeof scrollableTarget === 'string') {
12
+ return (
13
+ document.getElementById(scrollableTarget) || document.querySelector(scrollableTarget)
14
+ );
15
+ }
16
+
17
+ return scrollableTarget;
18
+ };
19
+
20
+ interface ObserverArgs {
21
+ testMode: boolean;
22
+ threshold: number;
23
+ scrollableTarget: ScrollableTarget;
24
+ }
25
+
26
+ /**
27
+ * Watches a zero-size sentinel at the end of the list. `rootMargin` is what makes
28
+ * it fire before the sentinel is actually on screen, so the next page is already
29
+ * loading by the time the user reaches the bottom.
30
+ */
31
+ export const useSentinel = ({ testMode, threshold, scrollableTarget }: ObserverArgs) => {
32
+ const sentinelRef = useRef<HTMLDivElement>(null);
33
+ const [isIntersecting, setIsIntersecting] = useState(false);
34
+
35
+ useEffect(() => {
36
+ if (testMode || !sentinelRef.current) return;
37
+
38
+ const observer = new window.IntersectionObserver(
39
+ ([entry]) => {
40
+ if (entry) {
41
+ setIsIntersecting(entry.isIntersecting);
42
+ }
43
+ },
44
+ {
45
+ root: resolveScrollable(scrollableTarget) || undefined,
46
+ rootMargin: `${threshold}px`,
47
+ threshold: 0,
48
+ },
49
+ );
50
+
51
+ observer.observe(sentinelRef.current);
52
+
53
+ return () => observer.disconnect();
54
+ }, [testMode, threshold, scrollableTarget]);
55
+
56
+ return { sentinelRef, isIntersecting };
57
+ };
58
+
59
+ type LoadArgs = Pick<
60
+ InfiniteScrollProps,
61
+ 'hasMore' | 'loading' | 'error' | 'loadMore' | 'onError' | 'testTriggerRef'
62
+ > & {
63
+ isIntersecting: boolean;
64
+ testMode: boolean;
65
+ };
66
+
67
+ /**
68
+ * A ref rather than state guards re-entry, so a second intersection landing while
69
+ * the first `loadMore` is still in flight is dropped rather than queued.
70
+ */
71
+ export const useLoadMore = ({
72
+ hasMore,
73
+ loading,
74
+ error,
75
+ loadMore,
76
+ onError,
77
+ isIntersecting,
78
+ testMode,
79
+ testTriggerRef,
80
+ }: LoadArgs) => {
81
+ const loadingRef = useRef(false);
82
+
83
+ const handleLoadMore = useCallback(async () => {
84
+ if (loadingRef.current || !hasMore || error) {
85
+ return;
86
+ }
87
+
88
+ loadingRef.current = true;
89
+
90
+ try {
91
+ await loadMore();
92
+ } catch (err) {
93
+ const errorObj = err instanceof Error ? err : new Error('Unknown error');
94
+ onError?.(errorObj);
95
+ } finally {
96
+ loadingRef.current = false;
97
+ }
98
+ }, [hasMore, error, loadMore, onError]);
99
+
100
+ // Expose test trigger for test mode
101
+ useEffect(() => {
102
+ if (testMode && testTriggerRef) {
103
+ testTriggerRef.current = handleLoadMore;
104
+ }
105
+
106
+ return () => {
107
+ if (testMode && testTriggerRef) {
108
+ testTriggerRef.current = undefined;
109
+ }
110
+ };
111
+ }, [testMode, testTriggerRef, handleLoadMore]);
112
+
113
+ useEffect(() => {
114
+ if (isIntersecting && hasMore && !loading && !error) {
115
+ handleLoadMore();
116
+ }
117
+ }, [isIntersecting, hasMore, loading, error, handleLoadMore]);
118
+ };
@@ -0,0 +1,54 @@
1
+ import { Alert, Box, CircularProgress, Typography } from '@mui/material';
2
+ import React from 'react';
3
+
4
+ import type { InfiniteScrollProps } from './InfiniteScroll.types';
5
+
6
+ type Variant = NonNullable<InfiniteScrollProps['variant']>;
7
+
8
+ const CENTERED = {
9
+ display: 'flex',
10
+ justifyContent: 'center',
11
+ alignItems: 'center',
12
+ p: 2,
13
+ } as const;
14
+
15
+ export const DefaultLoader: React.FC = () => (
16
+ <Box sx={CENTERED}>
17
+ <CircularProgress size={24} />
18
+ <Typography variant="body2" sx={{ ml: 1 }}>
19
+ Loading...
20
+ </Typography>
21
+ </Box>
22
+ );
23
+
24
+ export const DefaultEndMessage: React.FC = () => (
25
+ <Box sx={CENTERED}>
26
+ <Typography variant="body2" color="text.secondary">
27
+ No more items to load
28
+ </Typography>
29
+ </Box>
30
+ );
31
+
32
+ export const DefaultError: React.FC<{ error: Error }> = ({ error }) => (
33
+ <Alert severity="error" sx={{ m: 2 }}>
34
+ {error.message || 'An error occurred while loading more items'}
35
+ </Alert>
36
+ );
37
+
38
+ /**
39
+ * `horizontal` scrolls along a row; `reverse` stacks bottom-up, for a chat log
40
+ * that loads older messages as you scroll away from the newest.
41
+ */
42
+ export const containerStyles = (variant: Variant, width?: number | string) => {
43
+ const horizontal = variant === 'horizontal';
44
+ const base = {
45
+ width: horizontal ? width || '100%' : '100%',
46
+ height: horizontal ? '100%' : 'auto',
47
+ overflowX: horizontal ? 'auto' : 'hidden',
48
+ overflowY: horizontal ? 'hidden' : 'visible',
49
+ display: 'flex',
50
+ flexDirection: horizontal ? 'row' : 'column',
51
+ };
52
+
53
+ return variant === 'reverse' ? { ...base, flexDirection: 'column-reverse' } : base;
54
+ };
@@ -1,7 +1,7 @@
1
1
  import { Box, ListItem, ListItemText,Typography } from '@mui/material';
2
2
  import type { Meta, StoryObj } from '@storybook/react-vite';
3
3
  import React, { useCallback, useRef, useState } from 'react';
4
- import { expect, userEvent, waitFor,within } from 'storybook/test';
4
+ import { expect, userEvent, waitFor, within } from 'storybook/test';
5
5
 
6
6
  import { InfiniteScroll } from './InfiniteScroll';
7
7
 
@@ -45,7 +45,10 @@ const createTestComponent = (useTestMode = false) => {
45
45
  if (loadingRef.current) return; // Already loading
46
46
  loadingRef.current = true;
47
47
  setLoading(true);
48
- await new Promise((resolve) => setTimeout(resolve, 100));
48
+ await waitFor(() => {
49
+ // TODO: Add assertion or condition
50
+ expect(true).toBe(true);
51
+ });
49
52
  const newItems = generateItems(items.length, 3);
50
53
  setItems([...items, ...newItems]);
51
54
  setHasMore(items.length + 3 < 12);
@@ -107,7 +110,7 @@ export const BasicInteraction: Story = {
107
110
  await expect(canvas.getByTestId('item-4')).toBeInTheDocument();
108
111
 
109
112
  // Verify next batch items are NOT present yet
110
- await expect(canvas.queryByTestId('item-5')).not.toBeInTheDocument();
113
+ await waitFor(() => expect(canvas.queryByTestId('item-5')).not.toBeInTheDocument());
111
114
 
112
115
  // Verify sentinel is present and properly configured
113
116
  const sentinel = canvas.getByTestId('infinite-scroll-sentinel');
@@ -132,8 +135,8 @@ export const KeyboardNavigation: Story = {
132
135
  await step('Container focus verification', async () => {
133
136
  const container = canvas.getByTestId('scroll-container');
134
137
  container.setAttribute('tabindex', '0');
135
- container.focus();
136
- await expect(container).toHaveFocus();
138
+ await userEvent.click(container);
139
+ await waitFor(() => expect(container).toHaveFocus());
137
140
  });
138
141
  },
139
142
  };
@@ -166,13 +169,13 @@ export const FocusManagement: Story = {
166
169
 
167
170
  await step('Focus management verification', async () => {
168
171
  const firstItem = canvas.getByTestId('item-0');
169
- firstItem.focus();
170
- await expect(firstItem).toHaveFocus();
172
+ await userEvent.click(firstItem);
173
+ await waitFor(() => expect(firstItem).toHaveFocus());
171
174
 
172
175
  const container = canvas.getByTestId('scroll-container');
173
176
  container.setAttribute('tabindex', '0');
174
- container.focus();
175
- await expect(container).toHaveFocus();
177
+ await userEvent.click(container);
178
+ await waitFor(() => expect(container).toHaveFocus());
176
179
  });
177
180
  },
178
181
  };
@@ -1,6 +1,13 @@
1
- import { Alert,Box, CircularProgress, Typography } from '@mui/material';
2
- import React, { useCallback, useEffect, useRef, useState } from 'react';
3
-
1
+ import { Box } from '@mui/material';
2
+ import React from 'react';
3
+
4
+ import { useLoadMore, useSentinel } from './InfiniteScroll.hooks';
5
+ import {
6
+ containerStyles,
7
+ DefaultEndMessage,
8
+ DefaultError,
9
+ DefaultLoader,
10
+ } from './InfiniteScroll.parts';
4
11
  import type { InfiniteScrollProps } from './InfiniteScroll.types';
5
12
 
6
13
  export const InfiniteScroll: React.FC<InfiniteScrollProps> = ({
@@ -22,151 +29,25 @@ export const InfiniteScroll: React.FC<InfiniteScrollProps> = ({
22
29
  testMode = false,
23
30
  testTriggerRef,
24
31
  }) => {
25
- const [isIntersecting, setIsIntersecting] = useState(false);
26
- const sentinelRef = useRef<HTMLDivElement>(null);
27
- const loadingRef = useRef(false);
28
-
29
- const defaultLoader = (
30
- <Box
31
- sx={{
32
- display: 'flex',
33
- justifyContent: 'center',
34
- alignItems: 'center',
35
- p: 2,
36
- }}
37
- >
38
- <CircularProgress size={24} />
39
- <Typography variant="body2" sx={{ ml: 1 }}>
40
- Loading...
41
- </Typography>
42
- </Box>
43
- );
44
-
45
- const defaultEndMessage = (
46
- <Box
47
- sx={{
48
- display: 'flex',
49
- justifyContent: 'center',
50
- alignItems: 'center',
51
- p: 2,
52
- }}
53
- >
54
- <Typography variant="body2" color="text.secondary">
55
- No more items to load
56
- </Typography>
57
- </Box>
58
- );
59
-
60
- const defaultErrorComponent = error ? (
61
- <Alert severity="error" sx={{ m: 2 }}>
62
- {error.message || 'An error occurred while loading more items'}
63
- </Alert>
64
- ) : null;
65
-
66
- const handleLoadMore = useCallback(async () => {
67
- if (loadingRef.current || !hasMore || error) {
68
- return;
69
- }
70
-
71
- loadingRef.current = true;
72
-
73
- try {
74
- await loadMore();
75
- } catch (err) {
76
- const errorObj = err instanceof Error ? err : new Error('Unknown error');
77
- onError?.(errorObj);
78
- } finally {
79
- loadingRef.current = false;
80
- }
81
- }, [hasMore, error, loadMore, onError]);
82
-
83
- // Expose test trigger for test mode
84
- useEffect(() => {
85
- if (testMode && testTriggerRef) {
86
- testTriggerRef.current = handleLoadMore;
87
- }
88
-
89
- return () => {
90
- if (testMode && testTriggerRef) {
91
- testTriggerRef.current = undefined;
92
- }
93
- };
94
- }, [testMode, testTriggerRef, handleLoadMore]);
95
-
96
- useEffect(() => {
97
- if (testMode || !sentinelRef.current) return;
98
-
99
- const getScrollableElement = () => {
100
- if (!scrollableTarget) return null;
101
-
102
- if (typeof scrollableTarget === 'string') {
103
- return (
104
- document.getElementById(scrollableTarget) || document.querySelector(scrollableTarget)
105
- );
106
- }
107
-
108
- return scrollableTarget;
109
- };
110
-
111
- const scrollableElement = getScrollableElement();
112
- const rootElement = scrollableElement || undefined;
113
-
114
- const observer = new window.IntersectionObserver(
115
- ([entry]) => {
116
- if (entry) {
117
- setIsIntersecting(entry.isIntersecting);
118
- }
119
- },
120
- {
121
- root: rootElement,
122
- rootMargin: `${threshold}px`,
123
- threshold: 0,
124
- },
125
- );
126
-
127
- observer.observe(sentinelRef.current);
128
-
129
- return () => observer.disconnect();
130
- }, [threshold, scrollableTarget]);
131
-
132
- useEffect(() => {
133
- if (isIntersecting && hasMore && !loading && !error) {
134
- handleLoadMore();
135
- }
136
- }, [isIntersecting, hasMore, loading, error, handleLoadMore]);
137
-
138
- const getContainerStyles = () => {
139
- const baseStyles = {
140
- width: variant === 'horizontal' ? width || '100%' : '100%',
141
- height: variant === 'horizontal' ? '100%' : 'auto',
142
- overflowX: variant === 'horizontal' ? 'auto' : 'hidden',
143
- overflowY: variant === 'horizontal' ? 'hidden' : 'visible',
144
- display: 'flex',
145
- flexDirection: variant === 'horizontal' ? 'row' : 'column',
146
- };
147
-
148
- if (variant === 'reverse') {
149
- return {
150
- ...baseStyles,
151
- flexDirection: 'column-reverse',
152
- };
153
- }
154
-
155
- return baseStyles;
156
- };
157
-
32
+ const { sentinelRef, isIntersecting } = useSentinel({ testMode, threshold, scrollableTarget });
33
+
34
+ useLoadMore({
35
+ hasMore,
36
+ loading,
37
+ error,
38
+ loadMore,
39
+ onError,
40
+ isIntersecting,
41
+ testMode,
42
+ testTriggerRef,
43
+ });
44
+
45
+ // The tail of the list is one of four things: the end message, an error, a
46
+ // spinner, or the sentinel that asks for the next page.
158
47
  const renderSentinel = () => {
159
- if (!hasMore) {
160
- return endMessage || defaultEndMessage;
161
- }
162
-
163
- if (error) {
164
- return errorComponent || defaultErrorComponent;
165
- }
166
-
167
- if (loading) {
168
- return loader || defaultLoader;
169
- }
48
+ if (!hasMore) return endMessage || <DefaultEndMessage />;
49
+ if (error) return errorComponent || <DefaultError error={error} />;
50
+ if (loading) return loader || <DefaultLoader />;
170
51
 
171
52
  return (
172
53
  <div
@@ -184,7 +65,7 @@ export const InfiniteScroll: React.FC<InfiniteScrollProps> = ({
184
65
  <Box
185
66
  className={className}
186
67
  sx={{
187
- ...getContainerStyles(),
68
+ ...containerStyles(variant, width),
188
69
  ...style,
189
70
  }}
190
71
  >
@@ -0,0 +1,124 @@
1
+ import type { InstallPlatform } from './InstallPrompt.types';
2
+
3
+ /**
4
+ * Pure environment probes and dismissal storage for `usePwaInstall`.
5
+ *
6
+ * Everything here is a plain function taking its clock and its storage from the
7
+ * caller where that matters, so the hook's behaviour is testable without
8
+ * faking timers or a browser.
9
+ */
10
+
11
+ export const DEFAULT_STORAGE_KEY = 'pwa-install-dismissed';
12
+ export const DEFAULT_DISMISS_DAYS = 30;
13
+
14
+ const MS_PER_DAY = 86_400_000;
15
+ const STANDALONE_QUERY = '(display-mode: standalone)';
16
+
17
+ /** iOS Safari predates `display-mode` and exposes this instead. */
18
+ interface IosNavigator extends Navigator {
19
+ standalone?: boolean;
20
+ }
21
+
22
+ /** Guard for SSR and for jsdom runs where `matchMedia` is not implemented. */
23
+ const hasWindow = (): boolean => typeof window !== 'undefined';
24
+
25
+ /**
26
+ * True when the page is already running as an installed app.
27
+ *
28
+ * Both checks are needed: `display-mode: standalone` is the standard signal,
29
+ * but iOS never implemented it for home-screen launches and reports
30
+ * `navigator.standalone` instead. Miss the iOS branch and the installed app
31
+ * nags its own users to install it again.
32
+ */
33
+ export const isStandaloneDisplay = (): boolean => {
34
+ if (!hasWindow()) return false;
35
+ if ((window.navigator as IosNavigator).standalone === true) return true;
36
+ return window.matchMedia?.(STANDALONE_QUERY).matches === true;
37
+ };
38
+
39
+ /**
40
+ * True for Safari on iOS/iPadOS — the one browser that can install but cannot
41
+ * be asked programmatically.
42
+ *
43
+ * Two traps handled here. iPadOS 13+ reports a Macintosh user agent, so the
44
+ * touch-point count is what separates an iPad from a desktop; and Chrome,
45
+ * Firefox and Edge on iOS are Safari underneath but offer no Add to Home
46
+ * Screen at all, so telling their users to tap Share is a dead end.
47
+ *
48
+ * Kept parameterised so the agent table can be tested without reassigning
49
+ * `navigator`, which leaks across test files when it is not restored.
50
+ */
51
+ export const isIosSafariAgent = (userAgent: string, maxTouchPoints: number): boolean => {
52
+ const isIosDevice =
53
+ /iPad|iPhone|iPod/.test(userAgent) ||
54
+ (userAgent.includes('Macintosh') && maxTouchPoints > 1);
55
+
56
+ if (!isIosDevice) return false;
57
+
58
+ return !/CriOS|FxiOS|EdgiOS|OPiOS/.test(userAgent);
59
+ };
60
+
61
+ /** `isIosSafariAgent` against the live browser. Internal — callers use `resolveInstallPlatform`. */
62
+ const isIosSafari = (): boolean => {
63
+ if (!hasWindow()) return false;
64
+
65
+ const { userAgent, maxTouchPoints } = window.navigator;
66
+ return isIosSafariAgent(userAgent, maxTouchPoints);
67
+ };
68
+
69
+ /**
70
+ * Reads the stored dismissal timestamp.
71
+ *
72
+ * Storage access is wrapped because it THROWS rather than returning null in
73
+ * Safari private mode and wherever cookies are blocked — an uncaught throw
74
+ * here would take down the storefront on the very browsers this component
75
+ * exists to serve.
76
+ */
77
+ export const readDismissedAt = (storageKey: string): number | null => {
78
+ if (!hasWindow()) return null;
79
+
80
+ try {
81
+ const raw = window.localStorage.getItem(storageKey);
82
+ if (!raw) return null;
83
+
84
+ const parsed = Number.parseInt(raw, 10);
85
+ return Number.isFinite(parsed) ? parsed : null;
86
+ } catch {
87
+ return null;
88
+ }
89
+ };
90
+
91
+ /** Persists a dismissal. Silently a no-op when storage is unavailable. */
92
+ export const writeDismissedAt = (storageKey: string, at: number): void => {
93
+ if (!hasWindow()) return;
94
+
95
+ try {
96
+ window.localStorage.setItem(storageKey, String(at));
97
+ } catch {
98
+ // Storage blocked — the dismissal lasts for this page view only.
99
+ }
100
+ };
101
+
102
+ /**
103
+ * Whether a stored dismissal still suppresses the prompt.
104
+ *
105
+ * `dismissForDays: 0` means forever. A timestamp in the future (a device whose
106
+ * clock was wound back) is treated as active rather than expired, so a bad
107
+ * clock cannot turn into a prompt on every page view.
108
+ */
109
+ export const isDismissalActive = (
110
+ dismissedAt: number | null,
111
+ dismissForDays: number,
112
+ now: number,
113
+ ): boolean => {
114
+ if (dismissedAt === null) return false;
115
+ if (dismissForDays <= 0) return true;
116
+
117
+ return now - dismissedAt < dismissForDays * MS_PER_DAY;
118
+ };
119
+
120
+ /** Resolves which install route this browser offers. */
121
+ export const resolveInstallPlatform = (hasDeferredPrompt: boolean): InstallPlatform => {
122
+ if (hasDeferredPrompt) return 'prompt';
123
+ return isIosSafari() ? 'ios' : 'unsupported';
124
+ };