@12-apps/ui 1.0.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (448) hide show
  1. package/components-catalog.md +14 -0
  2. package/package.json +42 -27
  3. package/src/__tests__/data-grid.test.tsx +125 -0
  4. package/src/__tests__/package-wiring.test.ts +12 -8
  5. package/src/__tests__/social-login-button.test.tsx +5 -5
  6. package/src/__tests__/viewport.test.ts +50 -0
  7. package/src/charts/index.ts +8 -0
  8. package/src/components/data-display/Alert/Alert.md +1 -1
  9. package/src/components/data-display/Alert/Alert.stories.tsx +5 -7
  10. package/src/components/data-display/Alert/Alert.styles.tsx +255 -0
  11. package/src/components/data-display/Alert/Alert.test.stories.tsx +11 -13
  12. package/src/components/data-display/Alert/Alert.tsx +135 -328
  13. package/src/components/data-display/AlertDialog/AlertDialog.stories.tsx +3 -1
  14. package/src/components/data-display/AlertDialog/AlertDialog.tsx +191 -78
  15. package/src/components/data-display/AlertDialog/AlertDialog.types.ts +8 -1
  16. package/src/components/data-display/AsyncStateContainer/AsyncStateContainer.test.stories.tsx +5 -5
  17. package/src/components/data-display/AsyncStateContainer/AsyncStateContainer.tsx +44 -35
  18. package/src/components/data-display/Avatar/Avatar.stories.tsx +4 -1
  19. package/src/components/data-display/Avatar/Avatar.test.stories.tsx +5 -3
  20. package/src/components/data-display/Avatar/Avatar.tsx +154 -450
  21. package/src/components/data-display/Avatar/Avatar.view.tsx +396 -0
  22. package/src/components/data-display/Badge/Badge.animations.ts +67 -0
  23. package/src/components/data-display/Badge/Badge.helpers.ts +74 -0
  24. package/src/components/data-display/Badge/Badge.stories.tsx +14 -16
  25. package/src/components/data-display/Badge/Badge.styles.ts +261 -0
  26. package/src/components/data-display/Badge/Badge.test.stories.tsx +32 -31
  27. package/src/components/data-display/Badge/Badge.tsx +151 -557
  28. package/src/components/data-display/Badge/Badge.variants.ts +106 -0
  29. package/src/components/data-display/Badge/BadgeContent.tsx +107 -0
  30. package/src/components/data-display/Banner/Banner.stories.tsx +5 -1
  31. package/src/components/data-display/Banner/Banner.styles.ts +120 -0
  32. package/src/components/data-display/Banner/Banner.tsx +179 -224
  33. package/src/components/data-display/Chart/Chart.md +48 -1
  34. package/src/components/data-display/Chart/Chart.tsx +82 -500
  35. package/src/components/data-display/Chart/Chart.types.ts +2 -0
  36. package/src/components/data-display/Chart/ChartComposables.tsx +121 -0
  37. package/src/components/data-display/Chart/ChartSpec.types.ts +70 -0
  38. package/src/components/data-display/Chart/SpecChart.stories.tsx +150 -0
  39. package/src/components/data-display/Chart/SpecChart.test.stories.tsx +56 -0
  40. package/src/components/data-display/Chart/SpecChart.tsx +53 -0
  41. package/src/components/data-display/Chart/__tests__/SpecChart.test.tsx +60 -0
  42. package/src/components/data-display/Chart/__tests__/chart-format.test.ts +43 -0
  43. package/src/components/data-display/Chart/__tests__/chart-spec.test.ts +119 -0
  44. package/src/components/data-display/Chart/chart-format.ts +26 -0
  45. package/src/components/data-display/Chart/chart-internals.ts +148 -0
  46. package/src/components/data-display/Chart/chart-renderers.tsx +338 -0
  47. package/src/components/data-display/Chart/chart-spec-registry.ts +74 -0
  48. package/src/components/data-display/Chart/chart-spec.ts +171 -0
  49. package/src/components/data-display/Chart/index.ts +30 -0
  50. package/src/components/data-display/Chip/Chip.helpers.tsx +131 -0
  51. package/src/components/data-display/Chip/Chip.stories.tsx +2 -1
  52. package/src/components/data-display/Chip/Chip.test.stories.tsx +11 -8
  53. package/src/components/data-display/Chip/Chip.tsx +31 -94
  54. package/src/components/data-display/DataGrid/DataGrid.body.tsx +141 -0
  55. package/src/components/data-display/DataGrid/DataGrid.cells.tsx +238 -0
  56. package/src/components/data-display/DataGrid/DataGrid.model.ts +315 -0
  57. package/src/components/data-display/DataGrid/DataGrid.rows.ts +112 -0
  58. package/src/components/data-display/DataGrid/DataGrid.states.tsx +102 -0
  59. package/src/components/data-display/DataGrid/DataGrid.test.stories.tsx +6 -12
  60. package/src/components/data-display/DataGrid/DataGrid.tsx +129 -725
  61. package/src/components/data-display/DataViews/DataViewsGrid.tsx +187 -0
  62. package/src/components/data-display/DataViews/DataViewsTableBase.tsx +400 -0
  63. package/src/components/data-display/DataViews/ManageViewsDialog.tsx +140 -0
  64. package/src/components/data-display/DataViews/SaveViewModal.tsx +289 -0
  65. package/src/components/data-display/DataViews/__tests__/DataViewsGrid.test.tsx +271 -0
  66. package/src/components/data-display/DataViews/__tests__/DataViewsTableBase.test.tsx +134 -0
  67. package/src/components/data-display/DataViews/__tests__/SaveViewModal.test.tsx +83 -0
  68. package/src/components/data-display/DataViews/__tests__/data-views-filter.test.ts +115 -0
  69. package/src/components/data-display/DataViews/__tests__/data-views-preview.test.ts +53 -0
  70. package/src/components/data-display/DataViews/__tests__/range-pill.test.tsx +114 -0
  71. package/src/components/data-display/DataViews/base-card.stories.tsx +136 -0
  72. package/src/components/data-display/DataViews/base-card.tsx +278 -0
  73. package/src/components/data-display/DataViews/data-views-filter-panel.tsx +184 -0
  74. package/src/components/data-display/DataViews/data-views-filter.ts +138 -0
  75. package/src/components/data-display/DataViews/data-views-grid-helpers.tsx +248 -0
  76. package/src/components/data-display/DataViews/data-views-grid-parts.tsx +400 -0
  77. package/src/components/data-display/DataViews/data-views-inline-bar.tsx +257 -0
  78. package/src/components/data-display/DataViews/data-views-layout-context.tsx +176 -0
  79. package/src/components/data-display/DataViews/data-views-pagination.tsx +31 -0
  80. package/src/components/data-display/DataViews/data-views-preview.ts +53 -0
  81. package/src/components/data-display/DataViews/data-views-range-pill.tsx +277 -0
  82. package/src/components/data-display/DataViews/data-views-table-parts.tsx +150 -0
  83. package/src/components/data-display/DataViews/data-views-toolbar.tsx +149 -0
  84. package/src/components/data-display/DataViews/data-views-types.ts +312 -0
  85. package/src/components/data-display/DataViews/data-views-view-mutations.ts +68 -0
  86. package/src/components/data-display/DataViews/index.ts +33 -0
  87. package/src/components/data-display/DataViews/use-data-views-state.ts +349 -0
  88. package/src/components/data-display/DateTimeDisplay/DateTimeDisplay.hooks.ts +104 -0
  89. package/src/components/data-display/DateTimeDisplay/DateTimeDisplay.tsx +68 -118
  90. package/src/components/data-display/DescriptionItem/DescriptionItem.stories.tsx +1 -1
  91. package/src/components/data-display/DescriptionItem/DescriptionItem.tsx +52 -34
  92. package/src/components/data-display/EmptyState/EmptyState.test.stories.tsx +6 -6
  93. package/src/components/data-display/EmptyState/EmptyState.tsx +218 -135
  94. package/src/components/data-display/ErrorState/ErrorState.test.stories.tsx +2 -2
  95. package/src/components/data-display/ErrorState/ErrorState.tsx +83 -55
  96. package/src/components/data-display/HoverCard/HoverCard.stories.tsx +3 -1
  97. package/src/components/data-display/HoverCard/HoverCard.test.stories.tsx +5 -5
  98. package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.hooks.ts +99 -0
  99. package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.styles.ts +87 -0
  100. package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.tsx +28 -155
  101. package/src/components/data-display/LazyImage/LazyImage.hooks.ts +203 -0
  102. package/src/components/data-display/LazyImage/LazyImage.md +1 -1
  103. package/src/components/data-display/LazyImage/LazyImage.stories.tsx +3 -3
  104. package/src/components/data-display/LazyImage/LazyImage.tsx +144 -243
  105. package/src/components/data-display/Lightbox/Lightbox.constants.ts +53 -0
  106. package/src/components/data-display/Lightbox/Lightbox.helpers.ts +39 -0
  107. package/src/components/data-display/Lightbox/Lightbox.hooks.ts +259 -0
  108. package/src/components/data-display/Lightbox/Lightbox.test.stories.tsx +28 -35
  109. package/src/components/data-display/Lightbox/Lightbox.tsx +98 -658
  110. package/src/components/data-display/Lightbox/LightboxControls.tsx +152 -0
  111. package/src/components/data-display/Lightbox/LightboxCurrentMedia.tsx +50 -0
  112. package/src/components/data-display/Lightbox/LightboxMedia.tsx +76 -0
  113. package/src/components/data-display/Lightbox/LightboxOverlay.tsx +83 -0
  114. package/src/components/data-display/Lightbox/LightboxStage.tsx +97 -0
  115. package/src/components/data-display/Lightbox/LightboxThumbnails.tsx +83 -0
  116. package/src/components/data-display/Lightbox/useLightbox.ts +80 -0
  117. package/src/components/data-display/LoadingState/LoadingState.helpers.ts +52 -0
  118. package/src/components/data-display/LoadingState/LoadingState.tsx +117 -87
  119. package/src/components/data-display/MapPreview/MapControlBar.tsx +116 -0
  120. package/src/components/data-display/MapPreview/MapDecorations.tsx +155 -0
  121. package/src/components/data-display/MapPreview/MapLayers.tsx +70 -0
  122. package/src/components/data-display/MapPreview/MapMarkers.tsx +84 -0
  123. package/src/components/data-display/MapPreview/MapOverlays.tsx +68 -0
  124. package/src/components/data-display/MapPreview/MapPreview.constants.ts +19 -0
  125. package/src/components/data-display/MapPreview/MapPreview.helpers.ts +63 -0
  126. package/src/components/data-display/MapPreview/MapPreview.hooks.ts +265 -0
  127. package/src/components/data-display/MapPreview/MapPreview.test.stories.tsx +14 -10
  128. package/src/components/data-display/MapPreview/MapPreview.tsx +52 -882
  129. package/src/components/data-display/MapPreview/MapPreview.types.ts +8 -1
  130. package/src/components/data-display/MapPreview/MapSearchBar.tsx +64 -0
  131. package/src/components/data-display/MapPreview/MapTiles.tsx +70 -0
  132. package/src/components/data-display/MapPreview/StaticMapView.tsx +63 -0
  133. package/src/components/data-display/MapPreview/mapProjection.ts +52 -0
  134. package/src/components/data-display/MapPreview/mapSurface.ts +37 -0
  135. package/src/components/data-display/MapPreview/useMapPreview.ts +62 -0
  136. package/src/components/data-display/Popover/Popover.test.stories.tsx +13 -19
  137. package/src/components/data-display/Progress/Progress.helpers.ts +37 -0
  138. package/src/components/data-display/Progress/Progress.styles.ts +101 -0
  139. package/src/components/data-display/Progress/Progress.tsx +185 -213
  140. package/src/components/data-display/SectionOnboarding/SectionOnboarding.stories.tsx +1 -1
  141. package/src/components/data-display/SectionOnboarding/SectionOnboarding.tsx +1 -1
  142. package/src/components/data-display/StatCard/StatCard.md +117 -0
  143. package/src/components/data-display/StatCard/StatCard.stories.tsx +167 -0
  144. package/src/components/data-display/StatCard/StatCard.test.stories.tsx +240 -0
  145. package/src/components/data-display/StatCard/StatCard.tsx +236 -0
  146. package/src/components/data-display/StatCard/StatCard.types.ts +62 -0
  147. package/src/components/data-display/StatCard/index.ts +7 -0
  148. package/src/components/data-display/Stepper/Stepper.test.stories.tsx +12 -19
  149. package/src/components/data-display/Stepper/Stepper.tsx +1 -1
  150. package/src/components/data-display/Table/Table.helpers.ts +31 -0
  151. package/src/components/data-display/Table/Table.hooks.ts +161 -0
  152. package/src/components/data-display/Table/Table.stories.tsx +3 -1
  153. package/src/components/data-display/Table/Table.styles.ts +223 -0
  154. package/src/components/data-display/Table/Table.test.stories.tsx +4 -4
  155. package/src/components/data-display/Table/Table.tsx +291 -748
  156. package/src/components/data-display/Table/TableParts.hooks.tsx +112 -0
  157. package/src/components/data-display/Table/TableParts.tsx +298 -0
  158. package/src/components/data-display/Table/TableStates.tsx +21 -0
  159. package/src/components/data-display/Timeline/Timeline.stories.tsx +10 -12
  160. package/src/components/data-display/Timeline/Timeline.test.stories.tsx +30 -44
  161. package/src/components/data-display/Timeline/Timeline.tsx +23 -284
  162. package/src/components/data-display/Timeline/TimelineRow.tsx +346 -0
  163. package/src/components/data-display/TimingDiagram/TimingDiagram.tsx +38 -311
  164. package/src/components/data-display/TimingDiagram/TimingDiagram.views.tsx +346 -0
  165. package/src/components/data-display/Tooltip/Tooltip.stories.tsx +4 -1
  166. package/src/components/data-display/Tooltip/Tooltip.test.stories.tsx +44 -58
  167. package/src/components/data-display/Tooltip/Tooltip.tsx +67 -62
  168. package/src/components/data-display/index.ts +1 -0
  169. package/src/components/feedback/ConfirmAction/ConfirmAction.hooks.ts +96 -0
  170. package/src/components/feedback/ConfirmAction/ConfirmAction.md +140 -0
  171. package/src/components/feedback/ConfirmAction/ConfirmAction.parts.tsx +154 -0
  172. package/src/components/feedback/ConfirmAction/ConfirmAction.stories.tsx +146 -0
  173. package/src/components/feedback/ConfirmAction/ConfirmAction.test.stories.tsx +114 -0
  174. package/src/components/feedback/ConfirmAction/ConfirmAction.tsx +46 -0
  175. package/src/components/feedback/ConfirmAction/ConfirmAction.types.ts +111 -0
  176. package/src/components/feedback/ConfirmAction/ConfirmButton.tsx +18 -0
  177. package/src/components/feedback/ConfirmAction/__tests__/confirm-action.test.tsx +288 -0
  178. package/src/components/feedback/ConfirmAction/index.ts +16 -0
  179. package/src/components/feedback/ConfirmAction/with-confirmation.tsx +112 -0
  180. package/src/components/feedback/Dialog/Dialog.styles.ts +4 -2
  181. package/src/components/feedback/Dialog/Dialog.tsx +26 -1
  182. package/src/components/feedback/Dialog/__tests__/dialog-content-spacing.test.tsx +66 -0
  183. package/src/components/feedback/Modal/Modal.md +1 -1
  184. package/src/components/feedback/Modal/Modal.stories.tsx +2 -2
  185. package/src/components/feedback/Sonner/Sonner.tsx +104 -288
  186. package/src/components/feedback/Sonner/Sonner.types.ts +7 -0
  187. package/src/components/feedback/Sonner/SonnerToast.tsx +216 -0
  188. package/src/components/feedback/StackedModal/StackedModal.hooks.ts +226 -0
  189. package/src/components/feedback/StackedModal/StackedModal.parts.tsx +311 -0
  190. package/src/components/feedback/StackedModal/StackedModal.stories.tsx +13 -15
  191. package/src/components/feedback/StackedModal/StackedModal.styles.ts +193 -0
  192. package/src/components/feedback/StackedModal/StackedModal.tsx +100 -619
  193. package/src/components/feedback/StackedModal/StackedModal.types.ts +17 -3
  194. package/src/components/feedback/StackedModal/__tests__/stacked-modal-viewport.test.tsx +104 -0
  195. package/src/components/feedback/StackedModal/modal-stack.tsx +119 -0
  196. package/src/components/feedback/Toast/Toast.tsx +92 -81
  197. package/src/components/feedback/TutorialOverlay/TutorialOverlay.geometry.ts +65 -0
  198. package/src/components/feedback/TutorialOverlay/TutorialOverlay.hooks.ts +185 -0
  199. package/src/components/feedback/TutorialOverlay/TutorialOverlay.test.stories.tsx +2 -2
  200. package/src/components/feedback/TutorialOverlay/TutorialOverlay.tsx +156 -414
  201. package/src/components/feedback/TutorialOverlay/TutorialStepBody.tsx +255 -0
  202. package/src/components/feedback/index.ts +1 -0
  203. package/src/components/form/AddressAutocomplete/AddressAutocomplete.helpers.ts +58 -0
  204. package/src/components/form/AddressAutocomplete/AddressAutocomplete.hooks.ts +93 -0
  205. package/src/components/form/AddressAutocomplete/AddressAutocomplete.mapper.ts +57 -0
  206. package/src/components/form/AddressAutocomplete/AddressAutocomplete.stories.tsx +4 -2
  207. package/src/components/form/AddressAutocomplete/AddressAutocomplete.test.stories.tsx +20 -16
  208. package/src/components/form/AddressAutocomplete/AddressAutocomplete.tsx +26 -829
  209. package/src/components/form/AddressAutocomplete/AddressAutocomplete.types.ts +11 -0
  210. package/src/components/form/AddressAutocomplete/AddressAutocompleteField.tsx +86 -0
  211. package/src/components/form/AddressAutocomplete/AddressFallbackField.tsx +64 -0
  212. package/src/components/form/AddressAutocomplete/AddressInputField.tsx +80 -0
  213. package/src/components/form/AddressAutocomplete/AddressOption.tsx +83 -0
  214. package/src/components/form/AddressAutocomplete/AddressSuggestionsPaper.tsx +24 -0
  215. package/src/components/form/AddressAutocomplete/AddressTextField.tsx +43 -0
  216. package/src/components/form/AddressAutocomplete/mockPlaces.ts +208 -0
  217. package/src/components/form/AddressAutocomplete/useAddressPredictions.ts +136 -0
  218. package/src/components/form/AddressAutocomplete/useAddressSelection.ts +113 -0
  219. package/src/components/form/AddressAutocomplete/useCurrentLocation.ts +116 -0
  220. package/src/components/form/AddressAutocomplete/useGoogleMapsAPI.ts +75 -0
  221. package/src/components/form/Button/Button.helpers.ts +36 -0
  222. package/src/components/form/Button/Button.stories.tsx +4 -1
  223. package/src/components/form/Button/Button.styles.ts +225 -0
  224. package/src/components/form/Button/Button.test.stories.tsx +25 -21
  225. package/src/components/form/Button/Button.tsx +45 -252
  226. package/src/components/form/Calendar/Calendar.dates.ts +100 -0
  227. package/src/components/form/Calendar/Calendar.helpers.ts +32 -0
  228. package/src/components/form/Calendar/Calendar.hooks.ts +278 -0
  229. package/src/components/form/Calendar/Calendar.keyboard.ts +32 -0
  230. package/src/components/form/Calendar/Calendar.selection.ts +156 -0
  231. package/src/components/form/Calendar/Calendar.test.stories.tsx +33 -34
  232. package/src/components/form/Calendar/Calendar.tsx +63 -551
  233. package/src/components/form/Calendar/Calendar.types.ts +16 -0
  234. package/src/components/form/Calendar/CalendarDay.tsx +127 -0
  235. package/src/components/form/Calendar/CalendarDayCell.tsx +52 -0
  236. package/src/components/form/Calendar/CalendarMonth.tsx +128 -0
  237. package/src/components/form/CepField/CepField.md +138 -0
  238. package/src/components/form/CepField/CepField.stories.tsx +138 -0
  239. package/src/components/form/CepField/CepField.test.stories.tsx +281 -0
  240. package/src/components/form/CepField/CepField.tsx +211 -0
  241. package/src/components/form/CepField/CepField.types.ts +73 -0
  242. package/src/components/form/CepField/index.ts +2 -0
  243. package/src/components/form/Checkbox/Checkbox.helpers.ts +28 -0
  244. package/src/components/form/Checkbox/Checkbox.tsx +19 -16
  245. package/src/components/form/CodeEditor/CodeEditor.helpers.ts +45 -0
  246. package/src/components/form/CodeEditor/CodeEditor.hooks.ts +104 -0
  247. package/src/components/form/CodeEditor/CodeEditor.monaco.ts +109 -0
  248. package/src/components/form/CodeEditor/CodeEditor.test.stories.tsx +17 -18
  249. package/src/components/form/CodeEditor/CodeEditor.tsx +81 -330
  250. package/src/components/form/CodeEditor/CodeEditorToolbar.tsx +168 -0
  251. package/src/components/form/Command/Command.hooks.ts +207 -0
  252. package/src/components/form/Command/Command.parts.tsx +58 -0
  253. package/src/components/form/Command/Command.styles.ts +83 -0
  254. package/src/components/form/Command/Command.tsx +61 -270
  255. package/src/components/form/CreatableSelect/CreatableSelect.md +1 -1
  256. package/src/components/form/CreatableSelect/CreatableSelect.test.stories.tsx +1 -1
  257. package/src/components/form/CreatableSelect/CreatableSelect.tsx +152 -71
  258. package/src/components/form/CreatableSelect/CreatableSelect.types.ts +13 -0
  259. package/src/components/form/HeaderButton/HeaderButton.tsx +80 -0
  260. package/src/components/form/HeaderButton/index.ts +2 -0
  261. package/src/components/form/Input/Input.stories.tsx +5 -1
  262. package/src/components/form/Input/Input.styles.ts +176 -0
  263. package/src/components/form/Input/Input.test.stories.tsx +30 -23
  264. package/src/components/form/Input/Input.tsx +71 -211
  265. package/src/components/form/Input/Input.types.ts +11 -0
  266. package/src/components/form/InputOTP/InputOTP.test.stories.tsx +9 -17
  267. package/src/components/form/Menubar/Menubar.test.stories.tsx +4 -8
  268. package/src/components/form/PasswordStrength/PasswordStrength.helpers.tsx +117 -0
  269. package/src/components/form/PasswordStrength/PasswordStrength.test.stories.tsx +3 -3
  270. package/src/components/form/PasswordStrength/PasswordStrength.tsx +202 -255
  271. package/src/components/form/PhoneInput/PhoneInput.hooks.ts +134 -0
  272. package/src/components/form/PhoneInput/PhoneInput.test.stories.tsx +4 -4
  273. package/src/components/form/PhoneInput/PhoneInput.tsx +279 -292
  274. package/src/components/form/PhoneInput/countries.ts +54 -0
  275. package/src/components/form/PhoneInput/phone.ts +47 -0
  276. package/src/components/form/RadioGroup/RadioGroup.test.stories.tsx +9 -9
  277. package/src/components/form/RichTextEditor/RichTextEditor.dom.ts +124 -0
  278. package/src/components/form/RichTextEditor/RichTextEditor.format.ts +61 -0
  279. package/src/components/form/RichTextEditor/RichTextEditor.hooks.ts +142 -0
  280. package/src/components/form/RichTextEditor/RichTextEditor.test.stories.tsx +5 -12
  281. package/src/components/form/RichTextEditor/RichTextEditor.tsx +98 -410
  282. package/src/components/form/RichTextEditor/RichTextEditorToolbar.tsx +187 -0
  283. package/src/components/form/SearchPalette/SearchPalette.parts.tsx +22 -0
  284. package/src/components/form/SearchPalette/SearchPalette.tsx +13 -9
  285. package/src/components/form/Select/Select.test.stories.tsx +35 -27
  286. package/src/components/form/Slider/Slider.test.stories.tsx +9 -11
  287. package/src/components/form/Switch/Switch.md +2 -1
  288. package/src/components/form/Switch/Switch.test.stories.tsx +6 -6
  289. package/src/components/form/Textarea/Textarea.styles.ts +143 -0
  290. package/src/components/form/Textarea/Textarea.test.stories.tsx +20 -26
  291. package/src/components/form/Textarea/Textarea.tsx +114 -535
  292. package/src/components/form/Textarea/TextareaRichEditor.tsx +377 -0
  293. package/src/components/form/Toggle/Toggle.styles.ts +211 -0
  294. package/src/components/form/Toggle/Toggle.test.stories.tsx +19 -27
  295. package/src/components/form/Toggle/Toggle.tsx +10 -185
  296. package/src/components/form/UploadButton/UploadButton.hooks.ts +323 -0
  297. package/src/components/form/UploadButton/UploadButton.parts.tsx +210 -0
  298. package/src/components/form/UploadButton/UploadButton.stories.tsx +2 -1
  299. package/src/components/form/UploadButton/UploadButton.test.stories.tsx +123 -4
  300. package/src/components/form/UploadButton/UploadButton.tsx +76 -305
  301. package/src/components/form/UploadButton/UploadButton.types.ts +16 -0
  302. package/src/components/form/index.ts +1 -0
  303. package/src/components/form/total-form/Fields/CepField.tsx +143 -0
  304. package/src/components/form/total-form/Fields/ToggleField.tsx +61 -0
  305. package/src/components/form/total-form/Fields/index.ts +4 -0
  306. package/src/components/form/total-form/FormContainer.tsx +3 -3
  307. package/src/components/form/total-form/SubmitButton.tsx +25 -2
  308. package/src/components/form/total-form/total-form.test.tsx +118 -1
  309. package/src/components/layout/Accordion/Accordion.md +1 -1
  310. package/src/components/layout/Accordion/Accordion.stories.tsx +5 -1
  311. package/src/components/layout/Accordion/Accordion.styles.ts +80 -0
  312. package/src/components/layout/Accordion/Accordion.test.stories.tsx +32 -50
  313. package/src/components/layout/Accordion/Accordion.tsx +2 -75
  314. package/src/components/layout/Card/Card.helpers.ts +30 -0
  315. package/src/components/layout/Card/Card.stories.tsx +9 -11
  316. package/src/components/layout/Card/Card.styles.ts +182 -0
  317. package/src/components/layout/Card/Card.test.stories.tsx +46 -46
  318. package/src/components/layout/Card/Card.tsx +24 -241
  319. package/src/components/layout/CardGrid/CardGrid.md +96 -0
  320. package/src/components/layout/CardGrid/CardGrid.tsx +140 -0
  321. package/src/components/layout/CardGrid/__tests__/CardGrid.test.tsx +92 -0
  322. package/src/components/layout/CardGrid/index.ts +2 -0
  323. package/src/components/layout/Collapsible/Collapsible.helpers.ts +83 -0
  324. package/src/components/layout/Collapsible/Collapsible.test.stories.tsx +7 -3
  325. package/src/components/layout/Collapsible/Collapsible.tsx +111 -107
  326. package/src/components/layout/Container/Container.test.stories.tsx +11 -16
  327. package/src/components/layout/ContentToolbar/ColumnsMenu.tsx +1 -1
  328. package/src/components/layout/ContentToolbar/ContentToolbar.tsx +120 -44
  329. package/src/components/layout/ContentToolbar/ContentToolbar.types.ts +9 -0
  330. package/src/components/layout/ContentToolbar/FilterTrigger.tsx +1 -1
  331. package/src/components/layout/ContentToolbar/MultiSelectDropdown.tsx +58 -43
  332. package/src/components/layout/ContentToolbar/MultiSelectTriggers.tsx +139 -0
  333. package/src/components/layout/ContentToolbar/SavedViewsMenu.tsx +10 -3
  334. package/src/components/layout/ContentToolbar/SortByDropdown.tsx +35 -11
  335. package/src/components/layout/ContentToolbar/ViewSelector.tsx +5 -6
  336. package/src/components/layout/ContentToolbar/__tests__/MultiSelectDropdown.test.tsx +21 -0
  337. package/src/components/layout/Dashboard/Dashboard.test.stories.tsx +27 -1
  338. package/src/components/layout/Dashboard/Dashboard.tsx +27 -8
  339. package/src/components/layout/Dashboard/DashboardParts.tsx +12 -19
  340. package/src/components/layout/Dashboard/index.ts +2 -1
  341. package/src/components/layout/Drawer/Drawer.stories.tsx +8 -10
  342. package/src/components/layout/Drawer/Drawer.test.stories.tsx +18 -14
  343. package/src/components/layout/Drawer/Drawer.tsx +67 -53
  344. package/src/components/layout/Resizable/Resizable.helpers.ts +133 -0
  345. package/src/components/layout/Resizable/Resizable.hooks.ts +68 -0
  346. package/src/components/layout/Resizable/Resizable.tsx +38 -153
  347. package/src/components/layout/ScrollArea/ScrollArea.helpers.ts +51 -0
  348. package/src/components/layout/ScrollArea/ScrollArea.styles.ts +248 -0
  349. package/src/components/layout/ScrollArea/ScrollArea.tsx +183 -374
  350. package/src/components/layout/ScrollArea/ScrollAreaContent.tsx +70 -0
  351. package/src/components/layout/ScrollArea/ScrollViewport.tsx +90 -0
  352. package/src/components/layout/Separator/Separator.styles.ts +128 -0
  353. package/src/components/layout/Separator/Separator.test.stories.tsx +3 -12
  354. package/src/components/layout/Separator/Separator.tsx +6 -132
  355. package/src/components/layout/SettingsLayout/SettingsLayout.parts.tsx +2 -1
  356. package/src/components/layout/SettingsLayout/SettingsLayout.stories.tsx +6 -8
  357. package/src/components/layout/Sidebar/Sidebar.stories.tsx +10 -12
  358. package/src/components/layout/Sidebar/Sidebar.test.stories.tsx +35 -37
  359. package/src/components/layout/Skeleton/Skeleton.helpers.ts +33 -0
  360. package/src/components/layout/Skeleton/Skeleton.styles.ts +114 -0
  361. package/src/components/layout/Skeleton/Skeleton.test.stories.tsx +3 -3
  362. package/src/components/layout/Skeleton/Skeleton.tsx +67 -155
  363. package/src/components/layout/TableFilter/TableFilter.tsx +2 -1
  364. package/src/components/navigation/Breadcrumbs/BreadcrumbEntry.tsx +305 -0
  365. package/src/components/navigation/Breadcrumbs/Breadcrumbs.md +2 -1
  366. package/src/components/navigation/Breadcrumbs/Breadcrumbs.stories.tsx +12 -14
  367. package/src/components/navigation/Breadcrumbs/Breadcrumbs.styles.tsx +212 -0
  368. package/src/components/navigation/Breadcrumbs/Breadcrumbs.test.stories.tsx +62 -61
  369. package/src/components/navigation/Breadcrumbs/Breadcrumbs.tsx +116 -429
  370. package/src/components/navigation/CommandPalette/CommandPalette.helpers.ts +82 -0
  371. package/src/components/navigation/CommandPalette/CommandPalette.hooks.ts +202 -0
  372. package/src/components/navigation/CommandPalette/CommandPalette.stories.tsx +3 -1
  373. package/src/components/navigation/CommandPalette/CommandPalette.test.stories.tsx +3 -5
  374. package/src/components/navigation/CommandPalette/CommandPalette.tsx +108 -341
  375. package/src/components/navigation/CommandPalette/CommandPaletteResults.tsx +182 -0
  376. package/src/components/navigation/CommandPalette/ShortcutChip.tsx +18 -0
  377. package/src/components/navigation/ContextMenu/ContextMenu.md +3 -1
  378. package/src/components/navigation/ContextMenu/ContextMenu.stories.tsx +4 -1
  379. package/src/components/navigation/ContextMenu/ContextMenu.test.stories.tsx +19 -24
  380. package/src/components/navigation/ContextMenu/ContextMenu.tsx +70 -54
  381. package/src/components/navigation/DropdownMenu/DropdownMenu.md +3 -1
  382. package/src/components/navigation/DropdownMenu/DropdownMenu.stories.tsx +17 -19
  383. package/src/components/navigation/DropdownMenu/DropdownMenu.test.stories.tsx +26 -26
  384. package/src/components/navigation/DropdownMenu/DropdownMenu.tsx +189 -111
  385. package/src/components/navigation/NavigationMenu/NavigationMegaSection.tsx +66 -0
  386. package/src/components/navigation/NavigationMenu/NavigationMenu.md +7 -2
  387. package/src/components/navigation/NavigationMenu/NavigationMenu.shell.tsx +218 -0
  388. package/src/components/navigation/NavigationMenu/NavigationMenu.styles.ts +223 -0
  389. package/src/components/navigation/NavigationMenu/NavigationMenu.tsx +119 -808
  390. package/src/components/navigation/NavigationMenu/NavigationMenuItem.hooks.ts +126 -0
  391. package/src/components/navigation/NavigationMenu/NavigationMenuItem.tsx +352 -0
  392. package/src/components/navigation/NavigationMenu/NavigationStandardBody.tsx +148 -0
  393. package/src/components/navigation/NavigationMenu/stories/NavigationMenu.stories.tsx +15 -17
  394. package/src/components/navigation/NavigationMenu/stories/NavigationMenu.test.stories.tsx +27 -23
  395. package/src/components/navigation/NavigationMenu/stories/NavigationMenuHorizontal.stories.tsx +11 -13
  396. package/src/components/navigation/NavigationMenu/stories/NavigationMenuVertical.stories.tsx +9 -11
  397. package/src/components/navigation/Pagination/Pagination.helpers.tsx +85 -0
  398. package/src/components/navigation/Pagination/Pagination.md +2 -1
  399. package/src/components/navigation/Pagination/Pagination.stories.tsx +4 -6
  400. package/src/components/navigation/Pagination/Pagination.styles.ts +135 -0
  401. package/src/components/navigation/Pagination/Pagination.tsx +178 -313
  402. package/src/components/navigation/Pagination/PaginationParts.tsx +81 -0
  403. package/src/components/navigation/Tabs/Tabs.md +3 -1
  404. package/src/components/navigation/Tabs/Tabs.stories.tsx +21 -23
  405. package/src/components/navigation/Tabs/Tabs.styles.ts +163 -0
  406. package/src/components/navigation/Tabs/Tabs.test.stories.tsx +37 -37
  407. package/src/components/navigation/Tabs/Tabs.tsx +192 -361
  408. package/src/components/navigation/Tabs/TabsPanel.tsx +100 -0
  409. package/src/components/typography/Blockquote/Blockquote.tsx +1 -1
  410. package/src/components/typography/Code/Code.test.stories.tsx +5 -5
  411. package/src/components/typography/Code/Code.tsx +74 -49
  412. package/src/components/typography/Heading/Heading.test.stories.tsx +8 -8
  413. package/src/components/typography/Paragraph/Paragraph.test.stories.tsx +5 -12
  414. package/src/components/typography/Text/Text.tsx +84 -77
  415. package/src/components/utility/AnimatedIcon/AnimatedIcon.md +4 -4
  416. package/src/components/utility/AnimatedIcon/AnimatedIcon.stories.tsx +19 -21
  417. package/src/components/utility/AnimatedIcon/AnimatedIcon.test.stories.tsx +19 -12
  418. package/src/components/utility/AspectRatio/AspectRatio.test.stories.tsx +9 -9
  419. package/src/components/utility/InfiniteScroll/InfiniteScroll.hooks.ts +118 -0
  420. package/src/components/utility/InfiniteScroll/InfiniteScroll.parts.tsx +54 -0
  421. package/src/components/utility/InfiniteScroll/InfiniteScroll.test.stories.tsx +12 -9
  422. package/src/components/utility/InfiniteScroll/InfiniteScroll.tsx +29 -148
  423. package/src/components/utility/InstallPrompt/InstallPrompt.helpers.ts +124 -0
  424. package/src/components/utility/InstallPrompt/InstallPrompt.md +113 -0
  425. package/src/components/utility/InstallPrompt/InstallPrompt.stories.tsx +118 -0
  426. package/src/components/utility/InstallPrompt/InstallPrompt.styles.ts +50 -0
  427. package/src/components/utility/InstallPrompt/InstallPrompt.test.stories.tsx +83 -0
  428. package/src/components/utility/InstallPrompt/InstallPrompt.tsx +192 -0
  429. package/src/components/utility/InstallPrompt/InstallPrompt.types.ts +78 -0
  430. package/src/components/utility/InstallPrompt/__tests__/InstallPrompt.ios.test.tsx +65 -0
  431. package/src/components/utility/InstallPrompt/__tests__/usePwaInstall.test.tsx +271 -0
  432. package/src/components/utility/InstallPrompt/index.ts +10 -0
  433. package/src/components/utility/InstallPrompt/usePwaInstall.ts +171 -0
  434. package/src/components/utility/Transition/Transition.helpers.ts +40 -0
  435. package/src/components/utility/Transition/Transition.tsx +17 -68
  436. package/src/components/utility/VirtualList/VirtualList.hooks.ts +319 -0
  437. package/src/components/utility/VirtualList/VirtualList.test.stories.tsx +49 -38
  438. package/src/components/utility/VirtualList/VirtualList.tsx +110 -315
  439. package/src/components/utility/WorkflowStep/WorkflowStep.helpers.ts +151 -0
  440. package/src/components/utility/WorkflowStep/WorkflowStep.parts.tsx +371 -0
  441. package/src/components/utility/WorkflowStep/WorkflowStep.stories.tsx +9 -11
  442. package/src/components/utility/WorkflowStep/WorkflowStep.test.stories.tsx +7 -7
  443. package/src/components/utility/WorkflowStep/WorkflowStep.tsx +51 -449
  444. package/src/components/utility/WorkflowStep/WorkflowStepRow.tsx +96 -0
  445. package/src/components/utility/index.ts +1 -0
  446. package/src/mui/Link.ts +2 -0
  447. package/src/utils/index.ts +2 -1
  448. package/src/utils/viewport.ts +65 -0
@@ -0,0 +1,349 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+
5
+ import { type GridColumn } from "../DataGrid";
6
+ import { type SortFieldDefinition } from "../../layout/ContentToolbar";
7
+
8
+ import { filterRows } from "./data-views-filter";
9
+ import {
10
+ buildGridColumns,
11
+ buildRowActionsColumn,
12
+ computeActiveFilterCount,
13
+ defaultSortFields,
14
+ isActionsColumn,
15
+ } from "./data-views-grid-helpers";
16
+ import {
17
+ emptyViewState,
18
+ type DataViewColumn,
19
+ type DataViewServer,
20
+ type DataViewState,
21
+ type DataViewSyncState,
22
+ type FilterFieldConfig,
23
+ type RangeFieldConfig,
24
+ type RangeValue,
25
+ type RowAction,
26
+ } from "./data-views-types";
27
+
28
+ /** Inputs the hook needs to own the view state + derived data. */
29
+ interface UseDataViewsStateArgs<T extends Record<string, unknown>> {
30
+ rows: T[];
31
+ columns: DataViewColumn<T>[];
32
+ fields: FilterFieldConfig<T>[];
33
+ rangeFields: RangeFieldConfig<T>[];
34
+ getRowId: (row: T) => string | number;
35
+ onRowClick?: (row: T) => void;
36
+ appliedState?: DataViewState;
37
+ /**
38
+ * URL-driven controls (search/pills/ranges/sort) that re-apply on reference
39
+ * change while preserving column visibility — see {@link DataViewSyncState}.
40
+ */
41
+ syncState?: DataViewSyncState;
42
+ onStateChange?: (state: DataViewState) => void;
43
+ testIdPrefix: string;
44
+ sortFields?: SortFieldDefinition[];
45
+ rowActions?: RowAction<T>[];
46
+ rowActionsLeading?: (row: T) => React.ReactNode;
47
+ /** Bespoke per-row menu rendered in the actions column instead of the auto kebab. */
48
+ renderRowMenu?: (row: T) => React.ReactNode;
49
+ /**
50
+ * When present, the table is BACKEND-driven: `rows` is already the current
51
+ * page (no in-browser filter/sort/paginate), and query changes re-fetch via
52
+ * `server.onQueryChange`. Omit for the legacy client-side behaviour.
53
+ */
54
+ server?: DataViewServer;
55
+ }
56
+
57
+ /** A column the user may show/hide via the toolbar's ColumnsMenu. */
58
+ export interface HideableColumn {
59
+ id: string;
60
+ label: string;
61
+ }
62
+
63
+ /** Everything the DataViewsGrid render needs; keeps the component itself thin. */
64
+ export interface DataViewsController<T extends Record<string, unknown>> {
65
+ state: DataViewState;
66
+ ranges: Record<string, RangeValue>;
67
+ patch: (next: Partial<DataViewState>) => void;
68
+ matched: T[];
69
+ gridColumns: GridColumn<T>[];
70
+ /** Columns eligible for the show/hide menu (hideable, with a text header). */
71
+ hideableColumns: HideableColumn[];
72
+ /** Toggle one column's visibility in the current view state. */
73
+ toggleColumn: (id: string, visible: boolean) => void;
74
+ selectedIds: Set<string | number>;
75
+ setSelectedIds: (ids: Set<string | number>) => void;
76
+ selectedRows: T[];
77
+ clearSelection: () => void;
78
+ selectAll: () => void;
79
+ filterOpen: boolean;
80
+ setFilterOpen: (open: boolean) => void;
81
+ resolvedSortFields: SortFieldDefinition[];
82
+ activeSortField: string;
83
+ activeSortOrder: "asc" | "desc";
84
+ activeFilterCount: number;
85
+ /** True when the table is backend-driven (server mode). */
86
+ serverMode: boolean;
87
+ /** Server-mode pagination: 1-based current page (1 when client mode). */
88
+ serverPage: number;
89
+ /** Server-mode pagination: total pages (1 when client mode). */
90
+ serverPageCount: number;
91
+ /** Server-mode: total matched rows across all pages (drives the counter). */
92
+ serverTotalCount: number;
93
+ /** Server-mode: request a specific 1-based page (re-fetches via onQueryChange). */
94
+ changePage: (page: number) => void;
95
+ }
96
+
97
+ /**
98
+ * The columns the user may show/hide: explicitly hideable (not `hideable:false`,
99
+ * which marks structural columns like the image thumbnail or actions kebab) and
100
+ * carrying a text header to label the checkbox.
101
+ */
102
+ function computeHideableColumns<T extends Record<string, unknown>>(
103
+ columns: DataViewColumn<T>[],
104
+ ): HideableColumn[] {
105
+ return columns
106
+ .filter((col) => col.hideable !== false && typeof col.header === "string" && col.header !== "")
107
+ .map((col) => ({ id: col.id, label: col.header as string }));
108
+ }
109
+
110
+ /**
111
+ * Server-mode wiring extracted from {@link useDataViewsState} (keeps it within
112
+ * the size/complexity budget): re-fetch on every effective-query change (page
113
+ * resets to 1) and expose `changePage` for the pagination control. Read the
114
+ * latest `state`/`server` at fire time so an inline `server` object per render
115
+ * doesn't loop the effect.
116
+ */
117
+ function useServerQuery(server: DataViewServer | undefined, state: DataViewState): {
118
+ changePage: (page: number) => void;
119
+ } {
120
+ const emit = (page: number): void => {
121
+ if (!server) return;
122
+ server.onQueryChange({
123
+ search: state.search,
124
+ pills: state.pills,
125
+ ranges: state.ranges ?? {},
126
+ sortBy: state.sortBy,
127
+ page,
128
+ pageSize: server.pageSize,
129
+ });
130
+ };
131
+ const queryKey = server
132
+ ? JSON.stringify({ q: state.search, pills: state.pills, ranges: state.ranges ?? {}, sort: state.sortBy })
133
+ : "";
134
+ const firstQuery = useRef(true);
135
+ useEffect(() => {
136
+ if (!server) return;
137
+ if (firstQuery.current) {
138
+ firstQuery.current = false;
139
+ return;
140
+ }
141
+ emit(1);
142
+ // Emit only when the query key changes; `emit` reads fresh state at fire time.
143
+ }, [queryKey]);
144
+ return { changePage: (page: number) => emit(page) };
145
+ }
146
+
147
+ /** The controller's server-mode derived fields (or client-mode defaults). */
148
+ function serverDerived(
149
+ server: DataViewServer | undefined,
150
+ fallbackCount: number,
151
+ ): { serverMode: boolean; serverPage: number; serverPageCount: number; serverTotalCount: number } {
152
+ if (!server) {
153
+ return { serverMode: false, serverPage: 1, serverPageCount: 1, serverTotalCount: fallbackCount };
154
+ }
155
+ return {
156
+ serverMode: true,
157
+ serverPage: server.page,
158
+ serverPageCount: Math.max(1, Math.ceil(server.totalCount / server.pageSize)),
159
+ serverTotalCount: server.totalCount,
160
+ };
161
+ }
162
+
163
+ /**
164
+ * The rows to render: in server mode `rows` is already the current page (the
165
+ * backend filtered/sorted/paginated), so it passes through untouched; otherwise
166
+ * the client search/pill/range filter narrows it.
167
+ */
168
+ function useMatchedRows<T extends Record<string, unknown>>(
169
+ server: DataViewServer | undefined,
170
+ rows: T[],
171
+ columns: DataViewColumn<T>[],
172
+ fields: FilterFieldConfig<T>[],
173
+ rangeFields: RangeFieldConfig<T>[],
174
+ state: DataViewState,
175
+ ): T[] {
176
+ return useMemo(
177
+ () =>
178
+ server
179
+ ? rows
180
+ : filterRows(
181
+ rows,
182
+ columns,
183
+ fields,
184
+ { search: state.search, pills: state.pills, ranges: state.ranges },
185
+ rangeFields,
186
+ ),
187
+ [server, rows, columns, fields, rangeFields, state.search, state.pills, state.ranges],
188
+ );
189
+ }
190
+
191
+ /** Add or remove one column id from the visible-columns list. */
192
+ function nextVisibleColumns(current: string[], id: string, visible: boolean): string[] {
193
+ const without = current.filter((colId) => colId !== id);
194
+ return visible ? [...without, id] : without;
195
+ }
196
+
197
+ /**
198
+ * Reconcile external state changes and surface every change to the caller:
199
+ * - `appliedState` (a saved view) fully REPLACES the state — it is columns-
200
+ * authoritative, so applying a view restores its filters, sort AND columns.
201
+ * - `syncState` (URL-driven controls) MERGES over the current state, leaving
202
+ * `visibleColumns` untouched — so browser back/forward or a same-route
203
+ * deep-link re-applies search/pills/sort without wiping a user's hidden columns.
204
+ * Each channel only fires when its own reference changes (after the first run).
205
+ */
206
+ function useStateSyncEffects(
207
+ appliedState: DataViewState | undefined,
208
+ syncState: DataViewSyncState | undefined,
209
+ state: DataViewState,
210
+ onStateChange: ((state: DataViewState) => void) | undefined,
211
+ setState: React.Dispatch<React.SetStateAction<DataViewState>>,
212
+ ): void {
213
+ const firstApplied = useRef(true);
214
+ useEffect(() => {
215
+ if (firstApplied.current) {
216
+ firstApplied.current = false;
217
+ return;
218
+ }
219
+ if (appliedState) setState(appliedState);
220
+ // `setState` is a stable useState setter (passed in), included to satisfy
221
+ // exhaustive-deps; the effect still only re-applies when `appliedState` changes.
222
+ }, [appliedState, setState]);
223
+ const firstSync = useRef(true);
224
+ useEffect(() => {
225
+ if (firstSync.current) {
226
+ firstSync.current = false;
227
+ return;
228
+ }
229
+ // Merge — keep the current `visibleColumns` (never URL-carried); only the
230
+ // URL-driven controls change.
231
+ if (syncState) setState((prev) => ({ ...prev, ...syncState }));
232
+ }, [syncState, setState]);
233
+ useEffect(() => {
234
+ onStateChange?.(state);
235
+ }, [state, onStateChange]);
236
+ }
237
+
238
+ /**
239
+ * Append the auto kebab actions column unless the page already defines one. The
240
+ * column is added when there are `rowActions` OR a bespoke `renderRowMenu` (an
241
+ * entity's self-contained menu that owns its own popups).
242
+ */
243
+ function appendActionsColumn<T extends Record<string, unknown>>(
244
+ columns: DataViewColumn<T>[],
245
+ opts: {
246
+ rowActions?: RowAction<T>[];
247
+ rowActionsLeading?: (row: T) => React.ReactNode;
248
+ renderRowMenu?: (row: T) => React.ReactNode;
249
+ testIdPrefix: string;
250
+ getRowId: (row: T) => string | number;
251
+ },
252
+ ): DataViewColumn<T>[] {
253
+ const { rowActions, rowActionsLeading, renderRowMenu, testIdPrefix, getRowId } = opts;
254
+ if ((!rowActions && !renderRowMenu) || columns.some(isActionsColumn)) return columns;
255
+ return [
256
+ ...columns,
257
+ buildRowActionsColumn({
258
+ rowActions: rowActions ?? [],
259
+ leading: rowActionsLeading,
260
+ testIdPrefix,
261
+ getRowId,
262
+ renderRowMenu,
263
+ }),
264
+ ];
265
+ }
266
+
267
+ /**
268
+ * Owns the DataViews view state (search/pills/ranges/sort/visibleColumns),
269
+ * multi-select, the filter-panel toggle, and every derived value (filtered rows,
270
+ * grid columns, active sort/filter counts). Extracted from the component so the
271
+ * render stays within the size/complexity budget.
272
+ */
273
+ export function useDataViewsState<T extends Record<string, unknown>>({
274
+ rows,
275
+ columns,
276
+ fields,
277
+ rangeFields,
278
+ getRowId,
279
+ onRowClick,
280
+ appliedState,
281
+ syncState,
282
+ onStateChange,
283
+ testIdPrefix,
284
+ sortFields,
285
+ rowActions,
286
+ rowActionsLeading,
287
+ renderRowMenu,
288
+ server,
289
+ }: UseDataViewsStateArgs<T>): DataViewsController<T> {
290
+ // Append the auto kebab column unless the page defines one (see helper).
291
+ const columnsWithActions = useMemo(
292
+ () =>
293
+ appendActionsColumn(columns, { rowActions, rowActionsLeading, renderRowMenu, testIdPrefix, getRowId }),
294
+ [columns, rowActions, rowActionsLeading, renderRowMenu, testIdPrefix, getRowId],
295
+ );
296
+ const allColumnIds = useMemo(() => columnsWithActions.map((col) => col.id), [columnsWithActions]);
297
+ // Seed once: a saved view (`appliedState`) or a pristine all-columns state, with
298
+ // the URL-driven controls (`syncState`) merged over it so a deep-linked filter
299
+ // is reflected on first render. Column visibility is never URL-carried.
300
+ const [state, setState] = useState<DataViewState>(() => {
301
+ const seed = appliedState ?? emptyViewState(allColumnIds);
302
+ return syncState ? { ...seed, ...syncState } : seed;
303
+ });
304
+ const [selectedIds, setSelectedIdsState] = useState<Set<string | number>>(new Set());
305
+ const [filterOpen, setFilterOpen] = useState(false);
306
+
307
+ useStateSyncEffects(appliedState, syncState, state, onStateChange, setState);
308
+
309
+ const patch = (next: Partial<DataViewState>): void => setState((prev) => ({ ...prev, ...next }));
310
+ const matched = useMatchedRows(server, rows, columns, fields, rangeFields, state);
311
+ // Server mode: re-fetch on query change (page resets to 1) + drive pagination.
312
+ const { changePage } = useServerQuery(server, state);
313
+ const gridColumns = useMemo(
314
+ () => buildGridColumns(columnsWithActions, state.visibleColumns, onRowClick),
315
+ [columnsWithActions, onRowClick, state.visibleColumns],
316
+ );
317
+ const hideableColumns = useMemo(() => computeHideableColumns(columnsWithActions), [columnsWithActions]);
318
+ const toggleColumn = (id: string, visible: boolean): void =>
319
+ setState((prev) => ({ ...prev, visibleColumns: nextVisibleColumns(prev.visibleColumns, id, visible) }));
320
+ const selectedRows = useMemo(
321
+ () => matched.filter((row) => selectedIds.has(getRowId(row))),
322
+ [matched, selectedIds, getRowId],
323
+ );
324
+ const resolvedSortFields = sortFields ?? defaultSortFields(columns);
325
+ const activeSort = state.sortBy?.[0];
326
+
327
+ return {
328
+ state,
329
+ ranges: state.ranges ?? {},
330
+ patch,
331
+ matched,
332
+ gridColumns,
333
+ hideableColumns,
334
+ toggleColumn,
335
+ selectedIds,
336
+ setSelectedIds: setSelectedIdsState,
337
+ selectedRows,
338
+ clearSelection: () => setSelectedIdsState(new Set()),
339
+ selectAll: () => setSelectedIdsState(new Set(matched.map((row) => getRowId(row)))),
340
+ filterOpen,
341
+ setFilterOpen,
342
+ resolvedSortFields,
343
+ activeSortField: activeSort?.id ?? resolvedSortFields[0]?.value ?? "",
344
+ activeSortOrder: activeSort?.dir === "desc" ? "desc" : "asc",
345
+ activeFilterCount: computeActiveFilterCount(state, fields, rangeFields),
346
+ ...serverDerived(server, matched.length),
347
+ changePage,
348
+ };
349
+ }
@@ -0,0 +1,104 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import type { DateTimeDisplayProps } from './DateTimeDisplay.types';
4
+
5
+ type DateFormat = NonNullable<DateTimeDisplayProps['dateFormat']>;
6
+ type TimeFormat = NonNullable<DateTimeDisplayProps['timeFormat']>;
7
+
8
+ /**
9
+ * Format a date for display
10
+ */
11
+ function formatDatePart(date: Date, format: DateFormat): string {
12
+ const options: Intl.DateTimeFormatOptions = {
13
+ year: 'numeric',
14
+ month: format === 'numeric' ? '2-digit' : format === 'short' ? 'short' : 'long',
15
+ day: format === 'numeric' ? '2-digit' : 'numeric',
16
+ };
17
+ return date.toLocaleDateString(undefined, options);
18
+ }
19
+
20
+ /**
21
+ * Format time for display
22
+ */
23
+ function formatTimePart(date: Date, timeFormat: TimeFormat, showTimezone: boolean): string {
24
+ const options: Intl.DateTimeFormatOptions = {
25
+ hour: 'numeric',
26
+ minute: '2-digit',
27
+ hour12: timeFormat === '12h',
28
+ ...(showTimezone && { timeZoneName: 'short' }),
29
+ };
30
+ return date.toLocaleTimeString(undefined, options);
31
+ }
32
+
33
+ interface DateTimeParts {
34
+ /** null when no date was given or the given one does not parse. */
35
+ dateObj: Date | null;
36
+ formattedDate: string | null;
37
+ formattedTime: string | null;
38
+ /** The long form shown in the tooltip. */
39
+ fullDateTime: string | null;
40
+ }
41
+
42
+ interface DateTimePartsArgs {
43
+ date: DateTimeDisplayProps['date'];
44
+ dateFormat: DateFormat;
45
+ timeFormat: TimeFormat;
46
+ showTimezone: boolean;
47
+ }
48
+
49
+ const isValidDate = (date: Date | null): date is Date =>
50
+ date !== null && !isNaN(date.getTime());
51
+
52
+ /**
53
+ * A date landing exactly on midnight is taken to carry no time of day — callers
54
+ * pass date-only values as `new Date('2026-08-05')`, which parses to 00:00:00.
55
+ * The cost is that a genuine midnight timestamp renders without its time.
56
+ */
57
+ const hasTimeOfDay = (date: Date): boolean =>
58
+ !(date.getHours() === 0 && date.getMinutes() === 0 && date.getSeconds() === 0);
59
+
60
+ const longForm = (date: Date, withTime: boolean): string => {
61
+ const dateParts: Intl.DateTimeFormatOptions = {
62
+ weekday: 'long',
63
+ year: 'numeric',
64
+ month: 'long',
65
+ day: 'numeric',
66
+ };
67
+
68
+ if (!withTime) return date.toLocaleDateString(undefined, dateParts);
69
+
70
+ return date.toLocaleString(undefined, {
71
+ ...dateParts,
72
+ hour: 'numeric',
73
+ minute: '2-digit',
74
+ second: '2-digit',
75
+ timeZoneName: 'short',
76
+ });
77
+ };
78
+
79
+ export const useDateTimeParts = ({
80
+ date,
81
+ dateFormat,
82
+ timeFormat,
83
+ showTimezone,
84
+ }: DateTimePartsArgs): DateTimeParts => {
85
+ const dateObj = useMemo(() => {
86
+ if (!date) return null;
87
+ return date instanceof Date ? date : new Date(date);
88
+ }, [date]);
89
+
90
+ return useMemo(() => {
91
+ if (!isValidDate(dateObj)) {
92
+ return { dateObj: null, formattedDate: null, formattedTime: null, fullDateTime: null };
93
+ }
94
+
95
+ const withTime = hasTimeOfDay(dateObj);
96
+
97
+ return {
98
+ dateObj,
99
+ formattedDate: formatDatePart(dateObj, dateFormat),
100
+ formattedTime: withTime ? formatTimePart(dateObj, timeFormat, showTimezone) : null,
101
+ fullDateTime: longForm(dateObj, withTime),
102
+ };
103
+ }, [dateObj, dateFormat, timeFormat, showTimezone]);
104
+ };
@@ -1,37 +1,9 @@
1
1
  import { Box, Tooltip, Typography } from '@mui/material';
2
- import { forwardRef, useMemo } from 'react';
2
+ import { forwardRef } from 'react';
3
3
 
4
+ import { useDateTimeParts } from './DateTimeDisplay.hooks';
4
5
  import type { DateTimeDisplayProps } from './DateTimeDisplay.types';
5
6
 
6
- /**
7
- * Format a date for display
8
- */
9
- function formatDatePart(date: Date, format: 'short' | 'long' | 'numeric'): string {
10
- const options: Intl.DateTimeFormatOptions = {
11
- year: 'numeric',
12
- month: format === 'numeric' ? '2-digit' : format === 'short' ? 'short' : 'long',
13
- day: format === 'numeric' ? '2-digit' : 'numeric',
14
- };
15
- return date.toLocaleDateString(undefined, options);
16
- }
17
-
18
- /**
19
- * Format time for display
20
- */
21
- function formatTimePart(
22
- date: Date,
23
- timeFormat: '12h' | '24h',
24
- showTimezone: boolean,
25
- ): string {
26
- const options: Intl.DateTimeFormatOptions = {
27
- hour: 'numeric',
28
- minute: '2-digit',
29
- hour12: timeFormat === '12h',
30
- ...(showTimezone && { timeZoneName: 'short' }),
31
- };
32
- return date.toLocaleTimeString(undefined, options);
33
- }
34
-
35
7
  /**
36
8
  * Get font sizes based on size prop
37
9
  */
@@ -47,6 +19,57 @@ function getFontSizes(size: 'small' | 'medium' | 'large') {
47
19
  }
48
20
  }
49
21
 
22
+ interface StackedProps {
23
+ formattedDate: string | null;
24
+ formattedTime: string | null;
25
+ fontSizes: ReturnType<typeof getFontSizes>;
26
+ showTooltip: boolean;
27
+ sx: DateTimeDisplayProps['sx'];
28
+ dataTestId?: string;
29
+ }
30
+
31
+ const Stacked = forwardRef<HTMLSpanElement, StackedProps>(
32
+ ({ formattedDate, formattedTime, fontSizes, showTooltip, sx, dataTestId }, ref) => (
33
+ <Box
34
+ component="span"
35
+ ref={ref}
36
+ data-testid={dataTestId}
37
+ sx={{
38
+ display: 'inline-flex',
39
+ flexDirection: 'column',
40
+ lineHeight: 1.3,
41
+ cursor: showTooltip ? 'help' : 'default',
42
+ ...sx,
43
+ }}
44
+ >
45
+ <Typography
46
+ component="span"
47
+ sx={{
48
+ fontWeight: 500,
49
+ fontSize: fontSizes.date,
50
+ }}
51
+ data-testid={dataTestId ? `${dataTestId}-date` : undefined}
52
+ >
53
+ {formattedDate}
54
+ </Typography>
55
+ {formattedTime && (
56
+ <Typography
57
+ component="span"
58
+ sx={{
59
+ fontSize: fontSizes.time,
60
+ color: 'text.secondary',
61
+ }}
62
+ data-testid={dataTestId ? `${dataTestId}-time` : undefined}
63
+ >
64
+ {formattedTime}
65
+ </Typography>
66
+ )}
67
+ </Box>
68
+ ),
69
+ );
70
+
71
+ Stacked.displayName = 'DateTimeDisplayStacked';
72
+
50
73
  /**
51
74
  * DateTimeDisplay component - displays date and time in a stacked format
52
75
  * with date on one line and time on another.
@@ -66,63 +89,16 @@ export const DateTimeDisplay = forwardRef<HTMLSpanElement, DateTimeDisplayProps>
66
89
  },
67
90
  ref,
68
91
  ) => {
69
- const dateObj = useMemo(() => {
70
- if (!date) return null;
71
- return date instanceof Date ? date : new Date(date);
72
- }, [date]);
73
-
74
- // Auto-detect if time is midnight (no meaningful time)
75
- const hasTime = useMemo(() => {
76
- if (!dateObj || isNaN(dateObj.getTime())) return false;
77
- // If time is midnight (ignoring milliseconds), assume no time was provided
78
- const hours = dateObj.getHours();
79
- const minutes = dateObj.getMinutes();
80
- const seconds = dateObj.getSeconds();
81
- // Check if time is midnight (00:00:00) - ignore milliseconds
82
- const isMidnight = hours === 0 && minutes === 0 && seconds === 0;
83
- return !isMidnight;
84
- }, [dateObj]);
85
-
86
-
87
- const formattedDate = useMemo(() => {
88
- if (!dateObj || isNaN(dateObj.getTime())) return null;
89
- return formatDatePart(dateObj, dateFormat);
90
- }, [dateObj, dateFormat]);
91
-
92
- const formattedTime = useMemo(() => {
93
- if (!dateObj || isNaN(dateObj.getTime()) || !hasTime) return null;
94
- return formatTimePart(dateObj, timeFormat, showTimezone);
95
- }, [dateObj, hasTime, timeFormat, showTimezone]);
96
-
97
- const fullDateTime = useMemo(() => {
98
- if (!dateObj || isNaN(dateObj.getTime())) return null;
99
-
100
- // If no time to show, only show date in tooltip
101
- if (!hasTime) {
102
- return dateObj.toLocaleDateString(undefined, {
103
- weekday: 'long',
104
- year: 'numeric',
105
- month: 'long',
106
- day: 'numeric',
107
- });
108
- }
109
-
110
- return dateObj.toLocaleString(undefined, {
111
- weekday: 'long',
112
- year: 'numeric',
113
- month: 'long',
114
- day: 'numeric',
115
- hour: 'numeric',
116
- minute: '2-digit',
117
- second: '2-digit',
118
- timeZoneName: 'short',
119
- });
120
- }, [dateObj, hasTime]);
121
-
92
+ const { dateObj, formattedDate, formattedTime, fullDateTime } = useDateTimeParts({
93
+ date,
94
+ dateFormat,
95
+ timeFormat,
96
+ showTimezone,
97
+ });
122
98
  const fontSizes = getFontSizes(size);
123
99
 
124
100
  // Handle invalid or missing date
125
- if (!dateObj || isNaN(dateObj.getTime())) {
101
+ if (!dateObj) {
126
102
  return (
127
103
  <Box
128
104
  component="span"
@@ -136,41 +112,15 @@ export const DateTimeDisplay = forwardRef<HTMLSpanElement, DateTimeDisplayProps>
136
112
  }
137
113
 
138
114
  const content = (
139
- <Box
140
- component="span"
115
+ <Stacked
141
116
  ref={ref}
142
- data-testid={dataTestId}
143
- sx={{
144
- display: 'inline-flex',
145
- flexDirection: 'column',
146
- lineHeight: 1.3,
147
- cursor: showTooltip ? 'help' : 'default',
148
- ...sx,
149
- }}
150
- >
151
- <Typography
152
- component="span"
153
- sx={{
154
- fontWeight: 500,
155
- fontSize: fontSizes.date,
156
- }}
157
- data-testid={dataTestId ? `${dataTestId}-date` : undefined}
158
- >
159
- {formattedDate}
160
- </Typography>
161
- {formattedTime && (
162
- <Typography
163
- component="span"
164
- sx={{
165
- fontSize: fontSizes.time,
166
- color: 'text.secondary',
167
- }}
168
- data-testid={dataTestId ? `${dataTestId}-time` : undefined}
169
- >
170
- {formattedTime}
171
- </Typography>
172
- )}
173
- </Box>
117
+ formattedDate={formattedDate}
118
+ formattedTime={formattedTime}
119
+ fontSizes={fontSizes}
120
+ showTooltip={showTooltip}
121
+ sx={sx}
122
+ dataTestId={dataTestId}
123
+ />
174
124
  );
175
125
 
176
126
  if (!showTooltip) {
@@ -1,4 +1,4 @@
1
- import { ContentCopy as ContentCopyIcon } from '@mui/icons-material';
1
+ import ContentCopyIcon from '@mui/icons-material/ContentCopy';
2
2
  import { Box, Stack } from '@mui/material';
3
3
  import type { Meta, StoryObj } from '@storybook/react-vite';
4
4