@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,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { KeyboardArrowDown as ChevronDownIcon, Search as SearchIcon } from '@mui/icons-material';
3
+ import SearchIcon from '@mui/icons-material/Search';
4
4
  import {
5
5
  Box,
6
6
  Button,
@@ -11,7 +11,6 @@ import {
11
11
  Menu,
12
12
  MenuItem,
13
13
  TextField,
14
- Typography,
15
14
  } from '@mui/material';
16
15
  import React, { useMemo, useState } from 'react';
17
16
 
@@ -20,25 +19,7 @@ import type {
20
19
  MultiSelectExtraOption,
21
20
  MultiSelectOption,
22
21
  } from './ContentToolbar.types';
23
-
24
- /**
25
- * Trigger label from the current selection: `allLabel` when none/all are
26
- * selected, the single label when one, else "first (+N)". Uses Set iteration
27
- * order so the label reflects the first-clicked option.
28
- */
29
- function buildTriggerLabel<TValue extends string>(
30
- selected: ReadonlySet<TValue>,
31
- options: MultiSelectOption<TValue>[],
32
- allLabel: string,
33
- ): string {
34
- const size = selected.size;
35
- if (size === 0 || size === options.length) return allLabel;
36
- const labelMap = new Map(options.map((option) => [option.value, option.label]));
37
- const firstValue = selected.values().next().value as TValue;
38
- const firstLabel = labelMap.get(firstValue) ?? allLabel;
39
- if (size === 1) return firstLabel;
40
- return `${firstLabel} (+${size - 1})`;
41
- }
22
+ import { buildTriggerLabel, InlineTrigger, PillTrigger, StackedTrigger } from './MultiSelectTriggers';
42
23
 
43
24
  /** One checkbox menu row with an optional trailing count. */
44
25
  function CheckboxRow({
@@ -84,11 +65,13 @@ export function MultiSelectDropdown<TValue extends string = string>({
84
65
  searchable,
85
66
  searchPlaceholder = 'Search…',
86
67
  noResultsLabel = 'No results',
68
+ layout = 'inline',
87
69
  'data-testid': testId,
88
70
  }: MultiSelectDropdownProps<TValue>): React.JSX.Element {
89
71
  const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
90
72
  const [query, setQuery] = useState('');
91
73
  const open = Boolean(anchorEl);
74
+ const stacked = layout === 'stacked';
92
75
  // Reset the query each time the menu closes so it reopens showing everything.
93
76
  const close = (): void => {
94
77
  setAnchorEl(null);
@@ -100,24 +83,18 @@ export function MultiSelectDropdown<TValue extends string = string>({
100
83
  const anyExtraChecked = extraOptions?.some((option) => option.checked) ?? false;
101
84
  const clearDisabled = selected.size === 0 && !anyExtraChecked;
102
85
 
86
+ const openMenu = (event: React.MouseEvent<HTMLElement>): void => setAnchorEl(event.currentTarget);
87
+ const triggerProps = { triggerLabel, open, onOpen: openMenu, testId };
88
+
89
+ const renderTrigger = (): React.JSX.Element => {
90
+ if (stacked) return <StackedTrigger label={label} {...triggerProps} />;
91
+ if (layout === 'pill') return <PillTrigger label={label} selectedCount={selected.size} {...triggerProps} />;
92
+ return <InlineTrigger label={label} {...triggerProps} />;
93
+ };
94
+
103
95
  return (
104
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, fontSize: '0.875rem' }}>
105
- <Typography component="span" sx={{ color: 'text.secondary', fontSize: '0.875rem' }}>
106
- {label}:
107
- </Typography>
108
- <Button
109
- variant="text"
110
- size="small"
111
- color="inherit"
112
- data-testid={testId}
113
- aria-haspopup="menu"
114
- aria-expanded={open}
115
- onClick={(event) => setAnchorEl(event.currentTarget)}
116
- sx={{ minWidth: 0, height: 32, px: 1, gap: 0.5, color: 'text.primary', textTransform: 'none', fontWeight: 600 }}
117
- >
118
- {triggerLabel}
119
- <ChevronDownIcon sx={{ fontSize: 14 }} />
120
- </Button>
96
+ <>
97
+ {renderTrigger()}
121
98
  <MultiSelectMenu
122
99
  anchorEl={anchorEl}
123
100
  open={open}
@@ -133,9 +110,10 @@ export function MultiSelectDropdown<TValue extends string = string>({
133
110
  onQueryChange={setQuery}
134
111
  searchPlaceholder={searchPlaceholder}
135
112
  noResultsLabel={noResultsLabel}
113
+ matchAnchorWidth={stacked}
136
114
  testId={testId}
137
115
  />
138
- </Box>
116
+ </>
139
117
  );
140
118
  }
141
119
 
@@ -191,9 +169,24 @@ interface MultiSelectMenuProps<TValue extends string> {
191
169
  onQueryChange: (value: string) => void;
192
170
  searchPlaceholder: string;
193
171
  noResultsLabel: string;
172
+ /** Match the menu width to the trigger and left-align it (stacked select). */
173
+ matchAnchorWidth?: boolean;
194
174
  testId?: string;
195
175
  }
196
176
 
177
+ /**
178
+ * Menu placement. Default: anchored to the trigger's right (toolbar). When
179
+ * `matchAnchorWidth` (stacked select), match the trigger width and left-align so
180
+ * the menu sits flush under the full-width control.
181
+ */
182
+ function resolveMenuPlacement(
183
+ matchAnchorWidth: boolean | undefined,
184
+ anchorEl: HTMLElement | null,
185
+ ): { anchorWidth: number | undefined; align: 'left' | 'right' } {
186
+ if (!matchAnchorWidth) return { anchorWidth: undefined, align: 'right' };
187
+ return { anchorWidth: anchorEl?.offsetWidth, align: 'left' };
188
+ }
189
+
197
190
  /** The (optional) search box + checkbox list + optional "Options" section + a "Clear" footer. */
198
191
  function MultiSelectMenu<TValue extends string>({
199
192
  anchorEl,
@@ -210,6 +203,7 @@ function MultiSelectMenu<TValue extends string>({
210
203
  onQueryChange,
211
204
  searchPlaceholder,
212
205
  noResultsLabel,
206
+ matchAnchorWidth,
213
207
  testId,
214
208
  }: MultiSelectMenuProps<TValue>): React.JSX.Element {
215
209
  const trimmed = query.trim().toLowerCase();
@@ -217,18 +211,24 @@ function MultiSelectMenu<TValue extends string>({
217
211
  () => (trimmed ? options.filter((option) => option.label.toLowerCase().includes(trimmed)) : options),
218
212
  [options, trimmed],
219
213
  );
214
+ const { anchorWidth, align } = resolveMenuPlacement(matchAnchorWidth, anchorEl);
220
215
  return (
221
216
  <Menu
222
217
  anchorEl={anchorEl}
223
218
  open={open}
224
219
  onClose={close}
225
- anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
226
- transformOrigin={{ vertical: 'top', horizontal: 'right' }}
220
+ anchorOrigin={{ vertical: 'bottom', horizontal: align }}
221
+ transformOrigin={{ vertical: 'top', horizontal: align }}
227
222
  // Keep focus on the search box (don't let the Menu grab the first item),
228
223
  // and stop MUI's typeahead from swallowing keystrokes meant for the input.
229
224
  autoFocus={!showSearch}
230
225
  disableAutoFocusItem
231
- slotProps={{ paper: { sx: { minWidth: 180 } }, list: { sx: { pt: showSearch ? 0 : undefined } } }}
226
+ slotProps={{
227
+ // Cap the height so a long list (e.g. many categories) scrolls instead
228
+ // of running off-screen; the sticky search box stays pinned on top.
229
+ paper: { sx: { minWidth: anchorWidth ?? 180, width: anchorWidth, maxHeight: 360 } },
230
+ list: { sx: { pt: showSearch ? 0 : undefined, pb: 0 } },
231
+ }}
232
232
  >
233
233
  {showSearch && (
234
234
  <MenuSearchField query={query} onQueryChange={onQueryChange} placeholder={searchPlaceholder} testId={testId} />
@@ -277,7 +277,22 @@ function MenuClearFooter({
277
277
  testId?: string;
278
278
  }): React.JSX.Element {
279
279
  return (
280
- <Box sx={{ display: 'flex', justifyContent: 'flex-end', borderTop: 1, borderColor: 'divider', px: 1, py: 0.75 }}>
280
+ <Box
281
+ sx={{
282
+ // Pin to the bottom of the scroll viewport so it stays visible while the
283
+ // options list scrolls (mirrors the sticky search box on top).
284
+ position: 'sticky',
285
+ bottom: 0,
286
+ zIndex: 1,
287
+ display: 'flex',
288
+ justifyContent: 'flex-end',
289
+ borderTop: 1,
290
+ borderColor: 'divider',
291
+ bgcolor: 'background.paper',
292
+ px: 1,
293
+ py: 0.75,
294
+ }}
295
+ >
281
296
  <Button
282
297
  variant="text"
283
298
  size="small"
@@ -0,0 +1,139 @@
1
+ 'use client';
2
+
3
+ import ChevronDownIcon from '@mui/icons-material/KeyboardArrowDown';
4
+ import { Box, Button, Typography } from '@mui/material';
5
+ import React, { useId } from 'react';
6
+
7
+ import type { MultiSelectOption } from './ContentToolbar.types';
8
+
9
+ /**
10
+ * Trigger label from the current selection: `allLabel` when none/all are
11
+ * selected, the single label when one, else "first (+N)". Uses Set iteration
12
+ * order so the label reflects the first-clicked option.
13
+ */
14
+ export function buildTriggerLabel<TValue extends string>(
15
+ selected: ReadonlySet<TValue>,
16
+ options: MultiSelectOption<TValue>[],
17
+ allLabel: string,
18
+ ): string {
19
+ const size = selected.size;
20
+ if (size === 0 || size === options.length) return allLabel;
21
+ const labelMap = new Map(options.map((option) => [option.value, option.label]));
22
+ const firstValue = selected.values().next().value as TValue;
23
+ const firstLabel = labelMap.get(firstValue) ?? allLabel;
24
+ if (size === 1) return firstLabel;
25
+ return `${firstLabel} (+${size - 1})`;
26
+ }
27
+
28
+ export interface TriggerProps {
29
+ label: string;
30
+ triggerLabel: string;
31
+ open: boolean;
32
+ onOpen: (event: React.MouseEvent<HTMLElement>) => void;
33
+ testId?: string;
34
+ }
35
+
36
+ /** Compact `Label:` text button for a horizontal toolbar (the default layout). */
37
+ export function InlineTrigger({ label, triggerLabel, open, onOpen, testId }: TriggerProps): React.JSX.Element {
38
+ return (
39
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, fontSize: '0.875rem' }}>
40
+ <Typography component="span" sx={{ color: 'text.secondary', fontSize: '0.875rem' }}>
41
+ {label}:
42
+ </Typography>
43
+ <Button
44
+ variant="text"
45
+ size="small"
46
+ color="inherit"
47
+ data-testid={testId}
48
+ aria-haspopup="menu"
49
+ aria-expanded={open}
50
+ onClick={onOpen}
51
+ sx={{ minWidth: 0, height: 32, px: 1, gap: 0.5, color: 'text.primary', textTransform: 'none', fontWeight: 600 }}
52
+ >
53
+ {triggerLabel}
54
+ <ChevronDownIcon sx={{ fontSize: 14 }} />
55
+ </Button>
56
+ </Box>
57
+ );
58
+ }
59
+
60
+ /** Full-width, outlined select-style control with the label above (stacked layout). */
61
+ export function StackedTrigger({ label, triggerLabel, open, onOpen, testId }: TriggerProps): React.JSX.Element {
62
+ // Name the button by label + value (aria-labelledby): a <button> isn't labelable.
63
+ const baseId = useId();
64
+ return (
65
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
66
+ <Typography id={`${baseId}-label`} component="label" sx={{ fontSize: '0.75rem', fontWeight: 500, color: 'text.secondary' }}>
67
+ {label}
68
+ </Typography>
69
+ <Button
70
+ variant="outlined"
71
+ color="inherit"
72
+ fullWidth
73
+ data-testid={testId}
74
+ aria-haspopup="menu"
75
+ aria-expanded={open}
76
+ aria-labelledby={`${baseId}-label ${baseId}-value`}
77
+ onClick={onOpen}
78
+ endIcon={<ChevronDownIcon sx={{ fontSize: 18, color: 'text.secondary' }} />}
79
+ sx={{
80
+ justifyContent: 'space-between',
81
+ height: 40,
82
+ px: 1.5,
83
+ color: 'text.primary',
84
+ textTransform: 'none',
85
+ fontWeight: 400,
86
+ fontSize: '0.875rem',
87
+ borderColor: 'divider',
88
+ bgcolor: 'background.paper',
89
+ '&:hover': { borderColor: 'text.primary', bgcolor: 'background.paper' },
90
+ }}
91
+ >
92
+ <Box
93
+ component="span" id={`${baseId}-value`}
94
+ sx={{ flex: 1, minWidth: 0, textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
95
+ >
96
+ {triggerLabel}
97
+ </Box>
98
+ </Button>
99
+ </Box>
100
+ );
101
+ }
102
+
103
+ /** Rounded outlined chip (`Label ▾`, `Label (N) ▾` when selected) for a filter row. */
104
+ export function PillTrigger({
105
+ label,
106
+ open,
107
+ onOpen,
108
+ testId,
109
+ selectedCount,
110
+ }: TriggerProps & { selectedCount: number }): React.JSX.Element {
111
+ const active = selectedCount > 0;
112
+ return (
113
+ <Button
114
+ variant="outlined"
115
+ color="inherit"
116
+ size="small"
117
+ data-testid={testId}
118
+ aria-haspopup="menu"
119
+ aria-expanded={open}
120
+ onClick={onOpen}
121
+ endIcon={<ChevronDownIcon sx={{ fontSize: 16 }} />}
122
+ sx={{
123
+ borderRadius: 999,
124
+ height: 34,
125
+ px: 1.5,
126
+ color: 'text.primary',
127
+ fontWeight: 600,
128
+ fontSize: '0.8125rem',
129
+ textTransform: 'none',
130
+ borderColor: active ? 'primary.main' : 'divider',
131
+ bgcolor: active ? 'action.selected' : 'background.paper',
132
+ '&:hover': { borderColor: active ? 'primary.main' : 'text.primary', bgcolor: active ? 'action.selected' : 'action.hover' },
133
+ }}
134
+ >
135
+ {label}
136
+ {active ? ` (${selectedCount})` : ''}
137
+ </Button>
138
+ );
139
+ }
@@ -1,6 +1,9 @@
1
1
  'use client';
2
2
 
3
- import { Check as CheckIcon, KeyboardArrowDown as ChevronDownIcon, MoreVert as KebabIcon } from '@mui/icons-material';
3
+ import CheckIcon from '@mui/icons-material/Check';
4
+ import ChevronDownIcon from '@mui/icons-material/KeyboardArrowDown';
5
+ import KebabIcon from '@mui/icons-material/MoreVert';
6
+ import ViewIcon from '@mui/icons-material/Visibility';
4
7
  import { Box, Button, IconButton, Menu, MenuItem, Popover, Stack, Typography } from '@mui/material';
5
8
  import React, { useState } from 'react';
6
9
 
@@ -278,7 +281,6 @@ export function SavedViewsMenu<V extends SavedViewLike = SavedViewLike>({
278
281
  close();
279
282
  };
280
283
  const labels = { ...DEFAULT_LABELS, ...labelOverrides };
281
-
282
284
  const pinned = views.filter((view) => view.pinned);
283
285
  const recent = views.filter((view) => !view.pinned).slice(0, RECENT_LIMIT);
284
286
 
@@ -304,9 +306,14 @@ export function SavedViewsMenu<V extends SavedViewLike = SavedViewLike>({
304
306
  data-testid={`${testIdPrefix}-button`}
305
307
  aria-haspopup="menu"
306
308
  aria-expanded={Boolean(anchorEl)}
309
+ aria-label={activeViewName ?? labels.mainView}
307
310
  sx={{ minWidth: 0, height: 32, px: 1, gap: 0.5, color: 'text.primary', textTransform: 'none', fontWeight: 600 }}
308
311
  >
309
- {activeViewName ?? labels.mainView}
312
+ {/* Mobile: an eye icon stands in for the view name to keep the toolbar one line. */}
313
+ <ViewIcon sx={{ fontSize: 18, display: { xs: 'inline-flex', md: 'none' } }} />
314
+ <Box component="span" sx={{ display: { xs: 'none', md: 'inline' } }}>
315
+ {activeViewName ?? labels.mainView}
316
+ </Box>
310
317
  <ChevronDownIcon sx={{ fontSize: 14 }} />
311
318
  </Button>
312
319
  <Popover
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
- import { Check as CheckIcon, KeyboardArrowDown as ChevronDownIcon } from '@mui/icons-material';
3
+ import CheckIcon from '@mui/icons-material/Check';
4
+ import ChevronDownIcon from '@mui/icons-material/KeyboardArrowDown';
4
5
  import { Box, Button, Divider, ListSubheader, Menu, MenuItem, Typography } from '@mui/material';
5
6
  import React, { useState } from 'react';
6
7
 
@@ -45,6 +46,20 @@ function buildTriggerLabel(
45
46
  return `${fieldDef.label} (${orderOption.label.toLowerCase()})`;
46
47
  }
47
48
 
49
+ /** The trigger's full label (md+), short label (mobile), and direction arrow. */
50
+ function resolveTriggerParts(
51
+ fieldDef: SortFieldDefinition | undefined,
52
+ orderOption: SortOrderOption | undefined,
53
+ orderOptions: SortOrderOption[],
54
+ ): { full: string; short: string; arrowSuffix: string } {
55
+ const useRangeLabel = fieldDef?.triggerLabelStyle === 'range' && orderOptions.length === 2;
56
+ const orderIndex = orderOptions.findIndex((option) => option.value === orderOption?.value);
57
+ const arrow = resolveArrowDirection(orderOption, orderIndex);
58
+ const arrowSuffix = orderOptions.length > 0 && !useRangeLabel ? ` ${arrow === 'down' ? '↓' : '↑'}` : '';
59
+ const full = buildTriggerLabel(fieldDef, orderOption);
60
+ return { full, short: fieldDef?.label ?? full, arrowSuffix };
61
+ }
62
+
48
63
  const sectionLabelSx = { px: 1, py: 0.75, fontSize: '0.75rem', fontWeight: 600, color: 'text.primary', lineHeight: 1.5 } as const;
49
64
 
50
65
  /** A fixed-width check slot (empty when unchecked) so labels align. */
@@ -162,22 +177,24 @@ export function SortByDropdown<TField extends string = string>({
162
177
  const activeFieldDef = findActiveField(fields, activeField);
163
178
  const orderOptions = activeFieldDef?.orderOptions ?? [];
164
179
  const activeOrderOption = findActiveOrder(activeFieldDef, activeOrder);
165
- const useRangeLabel = activeFieldDef?.triggerLabelStyle === 'range' && orderOptions.length === 2;
166
-
167
- const orderIndex = orderOptions.findIndex((option) => option.value === activeOrderOption?.value);
168
- const arrow = resolveArrowDirection(activeOrderOption, orderIndex);
169
- const arrowSuffix = orderOptions.length > 0 && !useRangeLabel ? ` ${arrow === 'down' ? '↓' : '↑'}` : '';
180
+ const trigger = resolveTriggerParts(activeFieldDef, activeOrderOption, orderOptions);
170
181
 
171
182
  function handleFieldChange(field: TField): void {
183
+ // Switch field in ONE callback. Previously this also fired `onOrderChange`
184
+ // to reset the order, but a consumer whose `onOrderChange` derives the field
185
+ // from its own (now-stale) prop would re-apply the OLD field — silently
186
+ // reverting the field change (e.g. picking "E-mail" snapped back to "Nome").
187
+ // The field-change consumer owns the resulting order.
172
188
  onFieldChange(field);
173
- const nextOrder = safeFields.find((candidate) => candidate.value === field)?.orderOptions?.[0]?.value;
174
- if (nextOrder && onOrderChange) onOrderChange(nextOrder);
175
189
  close();
176
190
  }
177
191
 
178
192
  return (
179
193
  <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, fontSize: '0.875rem' }}>
180
- <Typography component="span" sx={{ color: 'text.secondary', fontSize: '0.875rem' }}>
194
+ <Typography
195
+ component="span"
196
+ sx={{ display: { xs: 'none', md: 'inline' }, color: 'text.secondary', fontSize: '0.875rem' }}
197
+ >
181
198
  Sort By:
182
199
  </Typography>
183
200
  <Button
@@ -190,8 +207,15 @@ export function SortByDropdown<TField extends string = string>({
190
207
  onClick={(event) => setAnchorEl(event.currentTarget)}
191
208
  sx={{ minWidth: 0, height: 32, px: 1, gap: 0.5, color: 'text.primary', textTransform: 'none', fontWeight: 600 }}
192
209
  >
193
- {buildTriggerLabel(activeFieldDef, activeOrderOption)}
194
- {arrowSuffix}
210
+ {/* Full "Nome (crescente)" on md+; just the field name on mobile — the
211
+ arrow already conveys the direction, so the parenthetical is dropped. */}
212
+ <Box component="span" sx={{ display: { xs: 'none', md: 'inline' } }}>
213
+ {trigger.full}
214
+ </Box>
215
+ <Box component="span" sx={{ display: { xs: 'inline', md: 'none' } }}>
216
+ {trigger.short}
217
+ </Box>
218
+ {trigger.arrowSuffix}
195
219
  <ChevronDownIcon sx={{ fontSize: 14 }} />
196
220
  </Button>
197
221
  <SortMenu
@@ -1,12 +1,11 @@
1
1
  'use client';
2
2
 
3
- import {
4
- Check as CheckIcon,
5
- KeyboardArrowDown as ChevronDownIcon,
6
- GridViewOutlined as GridIcon,
7
- FormatListBulletedRounded as ListIcon,
8
- } from '@mui/icons-material';
3
+ import CheckIcon from '@mui/icons-material/Check';
4
+ import ChevronDownIcon from '@mui/icons-material/KeyboardArrowDown';
5
+ import GridIcon from '@mui/icons-material/GridViewOutlined';
6
+ import ListIcon from '@mui/icons-material/FormatListBulletedRounded';
9
7
  import { Box, Button, ListItemIcon, ListItemText, Menu, MenuItem, Slider } from '@mui/material';
8
+ // Type-only, so it is erased at build time and costs no barrel transform.
10
9
  import type { SvgIconComponent } from '@mui/icons-material';
11
10
  import React, { useState } from 'react';
12
11
 
@@ -108,3 +108,24 @@ describe('MultiSelectDropdown search (FUT-169)', () => {
108
108
  expect(screen.getByTestId('cat-search')).toBeInTheDocument();
109
109
  });
110
110
  });
111
+
112
+ describe('MultiSelectDropdown stacked trigger accessibility', () => {
113
+ it("names the trigger by the field label AND value so it isn't announced as just its value", () => {
114
+ render(
115
+ <MultiSelectDropdown
116
+ label="Papéis"
117
+ layout="stacked"
118
+ options={MANY_OPTIONS}
119
+ selected={new Set<string>()}
120
+ onToggle={vi.fn()}
121
+ onClear={vi.fn()}
122
+ allLabel="Todas"
123
+ data-testid="roles"
124
+ />,
125
+ );
126
+
127
+ // The accessible name resolves aria-labelledby to "<label> <value>", so the
128
+ // field name (not just "Todas") distinguishes this trigger from a sibling.
129
+ expect(screen.getByRole('button', { name: 'Papéis Todas' })).toBe(screen.getByTestId('roles'));
130
+ });
131
+ });
@@ -2,7 +2,7 @@ import { Box, Button, TextField, Typography } from '@mui/material';
2
2
  import type { Meta, StoryObj } from '@storybook/react-vite';
3
3
  import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
4
4
 
5
- import { Dashboard } from './Dashboard';
5
+ import { Dashboard, markDashboardSlot } from './Dashboard';
6
6
 
7
7
  const meta: Meta<typeof Dashboard> = {
8
8
  title: 'Layout/Dashboard/Tests',
@@ -162,3 +162,29 @@ export const OrderIndependentRendering: Story = {
162
162
  });
163
163
  },
164
164
  };
165
+
166
+ /** A page-owned wrapper around `<Dashboard.Header>`, tagged with its slot. */
167
+ const WrappedHeader = markDashboardSlot(
168
+ ({ title }: { title: string }) => <Dashboard.Header title={title} />,
169
+ 'header',
170
+ );
171
+
172
+ export const MarkedWrapperKeepsHeaderAboveBody: Story = {
173
+ name: '🧪 A slot-marked header wrapper still renders above the body',
174
+ render: () => (
175
+ <Dashboard>
176
+ <Dashboard.Breadcrumb items={[{ label: 'Admin', href: '#' }, { label: 'Wrapped' }]} />
177
+ <WrappedHeader title="Wrapped" />
178
+ <Dashboard.Body>{body}</Dashboard.Body>
179
+ </Dashboard>
180
+ ),
181
+ play: async ({ canvasElement, step }) => {
182
+ const canvas = within(canvasElement);
183
+ await step('Wrapped header sits above the body, not below it', async () => {
184
+ const header = canvas.getByTestId('dashboard-header');
185
+ const content = canvas.getByTestId('dashboard-body');
186
+ const headerBeforeBody = header.compareDocumentPosition(content) & Node.DOCUMENT_POSITION_FOLLOWING;
187
+ await expect(Boolean(headerBeforeBody)).toBe(true);
188
+ });
189
+ },
190
+ };
@@ -1,11 +1,9 @@
1
1
  'use client';
2
2
 
3
- import {
4
- ExpandLess as ExpandLessIcon,
5
- ExpandMore as ExpandMoreIcon,
6
- FilterListRounded as FilterIcon,
7
- NavigateNext as NavigateNextIcon,
8
- } from '@mui/icons-material';
3
+ import ExpandLessIcon from '@mui/icons-material/ExpandLess';
4
+ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
5
+ import FilterIcon from '@mui/icons-material/FilterListRounded';
6
+ import NavigateNextIcon from '@mui/icons-material/NavigateNext';
9
7
  import {
10
8
  Box,
11
9
  Breadcrumbs as MuiBreadcrumbs,
@@ -41,10 +39,31 @@ import type {
41
39
  * a `<Dashboard.Header>` still renders below it.
42
40
  */
43
41
  const SLOT_ORDER = ['breadcrumb', 'header', 'filters', 'body'] as const;
44
- type SlotKey = (typeof SLOT_ORDER)[number];
42
+ /** A slot a component can occupy in the canonical Dashboard order. */
43
+ export type DashboardSlot = (typeof SLOT_ORDER)[number];
45
44
 
46
45
  /** Marker attached to each sub-component so the root can rank it by slot. */
47
- type SlottedComponent = { dashboardSlot?: SlotKey };
46
+ type SlottedComponent = { dashboardSlot?: DashboardSlot };
47
+
48
+ /**
49
+ * Tag a component with the Dashboard slot it occupies so the root ranks it in
50
+ * canonical order. The root inspects only its DIRECT children's `type`, so a
51
+ * page that wraps a `<Dashboard.Header>` (or any part) in its own component MUST
52
+ * mark that wrapper — otherwise it counts as an unrecognised child and sinks
53
+ * below the body. Returns the component for convenient inline use.
54
+ *
55
+ * @example
56
+ * ```tsx
57
+ * function ProductsHeader(props) {
58
+ * return <Dashboard.Header title="Products">{/* … *\/}</Dashboard.Header>;
59
+ * }
60
+ * markDashboardSlot(ProductsHeader, 'header');
61
+ * ```
62
+ */
63
+ export function markDashboardSlot<T>(component: T, slot: DashboardSlot): T {
64
+ (component as SlottedComponent).dashboardSlot = slot;
65
+ return component;
66
+ }
48
67
 
49
68
  const Root = styled(Box)(({ theme }) => ({
50
69
  display: 'flex',
@@ -1,17 +1,14 @@
1
1
  'use client';
2
2
 
3
- import {
4
- Close as CloseIcon,
5
- ExpandMore as ExpandMoreIcon,
6
- FileDownloadOutlined as ExportIcon,
7
- FilterListRounded as FilterIcon,
8
- InfoOutlined as InfoIcon,
9
- SettingsOutlined as SettingsIcon,
10
- } from '@mui/icons-material';
3
+ import CloseIcon from '@mui/icons-material/Close';
4
+ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
5
+ import ExportIcon from '@mui/icons-material/FileDownloadOutlined';
6
+ import FilterIcon from '@mui/icons-material/FilterListRounded';
7
+ import InfoIcon from '@mui/icons-material/InfoOutlined';
8
+ import SettingsIcon from '@mui/icons-material/SettingsOutlined';
11
9
  import {
12
10
  Badge,
13
11
  Box,
14
- Button,
15
12
  Dialog,
16
13
  DialogContent,
17
14
  DialogTitle,
@@ -24,6 +21,7 @@ import {
24
21
  } from '@mui/material';
25
22
  import React, { useState } from 'react';
26
23
 
24
+ import { HeaderButton } from '../../form/HeaderButton';
27
25
  import { useDashboardContext } from './DashboardContext';
28
26
  import type {
29
27
  DashboardActionProps,
@@ -175,22 +173,17 @@ export const DashboardExport = ({
175
173
  const open = Boolean(anchorEl);
176
174
  return (
177
175
  <>
178
- <Button
176
+ <HeaderButton
179
177
  variant="outlined"
180
- size="medium"
181
178
  color="inherit"
182
- startIcon={<ExportIcon fontSize="small" />}
179
+ text={label}
180
+ icon={<ExportIcon fontSize="small" />}
183
181
  endIcon={<ExpandMoreIcon fontSize="small" />}
184
182
  onClick={(e) => setAnchorEl(e.currentTarget)}
185
183
  aria-haspopup="menu"
186
184
  aria-expanded={open}
187
- data-testid={`${testIdPrefix}-export-trigger`}
188
- // Match the sibling primary action button (repo Button `md`): same
189
- // padding/height and a 1rem non-uppercased label.
190
- sx={{ py: 1, px: 2, fontSize: '1rem', textTransform: 'none' }}
191
- >
192
- {label}
193
- </Button>
185
+ dataTestId={`${testIdPrefix}-export-trigger`}
186
+ />
194
187
  <Menu anchorEl={anchorEl} open={open} onClose={() => setAnchorEl(null)} data-testid={`${testIdPrefix}-export-menu`}>
195
188
  {formats.map((format) => (
196
189
  <MenuItem
@@ -1,4 +1,5 @@
1
- export { Dashboard } from './Dashboard';
1
+ export { Dashboard, markDashboardSlot } from './Dashboard';
2
+ export type { DashboardSlot } from './Dashboard';
2
3
  export { DashboardContext, useDashboardContext } from './DashboardContext';
3
4
  export type { DashboardContextValue } from './DashboardContext';
4
5
  export type {
@@ -1,13 +1,11 @@
1
- import {
2
- Analytics,
3
- Dashboard,
4
- Help,
5
- Home,
6
- Menu as MenuIcon,
7
- Notifications,
8
- Person,
9
- Settings,
10
- } from '@mui/icons-material';
1
+ import Analytics from '@mui/icons-material/Analytics';
2
+ import Dashboard from '@mui/icons-material/Dashboard';
3
+ import Help from '@mui/icons-material/Help';
4
+ import Home from '@mui/icons-material/Home';
5
+ import MenuIcon from '@mui/icons-material/Menu';
6
+ import Notifications from '@mui/icons-material/Notifications';
7
+ import Person from '@mui/icons-material/Person';
8
+ import Settings from '@mui/icons-material/Settings';
11
9
  import {
12
10
  Box,
13
11
  Button,