@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,54 @@
1
+ import type { CountryCode } from 'libphonenumber-js';
2
+
3
+ import type { CountryData } from './PhoneInput.types';
4
+
5
+ // Dial codes and flags for the countries the input offers, sorted by name.
6
+ export const countries: CountryData[] = [
7
+ { code: 'US' as CountryCode, name: 'United States', dial: '+1', flag: '🇺🇸' },
8
+ { code: 'GB' as CountryCode, name: 'United Kingdom', dial: '+44', flag: '🇬🇧' },
9
+ { code: 'CA' as CountryCode, name: 'Canada', dial: '+1', flag: '🇨🇦' },
10
+ { code: 'AU' as CountryCode, name: 'Australia', dial: '+61', flag: '🇦🇺' },
11
+ { code: 'DE' as CountryCode, name: 'Germany', dial: '+49', flag: '🇩🇪' },
12
+ { code: 'FR' as CountryCode, name: 'France', dial: '+33', flag: '🇫🇷' },
13
+ { code: 'IT' as CountryCode, name: 'Italy', dial: '+39', flag: '🇮🇹' },
14
+ { code: 'ES' as CountryCode, name: 'Spain', dial: '+34', flag: '🇪🇸' },
15
+ { code: 'NL' as CountryCode, name: 'Netherlands', dial: '+31', flag: '🇳🇱' },
16
+ { code: 'JP' as CountryCode, name: 'Japan', dial: '+81', flag: '🇯🇵' },
17
+ { code: 'CN' as CountryCode, name: 'China', dial: '+86', flag: '🇨🇳' },
18
+ { code: 'IN' as CountryCode, name: 'India', dial: '+91', flag: '🇮🇳' },
19
+ { code: 'BR' as CountryCode, name: 'Brazil', dial: '+55', flag: '🇧🇷' },
20
+ { code: 'MX' as CountryCode, name: 'Mexico', dial: '+52', flag: '🇲🇽' },
21
+ { code: 'KR' as CountryCode, name: 'South Korea', dial: '+82', flag: '🇰🇷' },
22
+ { code: 'SE' as CountryCode, name: 'Sweden', dial: '+46', flag: '🇸🇪' },
23
+ { code: 'NO' as CountryCode, name: 'Norway', dial: '+47', flag: '🇳🇴' },
24
+ { code: 'DK' as CountryCode, name: 'Denmark', dial: '+45', flag: '🇩🇰' },
25
+ { code: 'CH' as CountryCode, name: 'Switzerland', dial: '+41', flag: '🇨🇭' },
26
+ { code: 'AT' as CountryCode, name: 'Austria', dial: '+43', flag: '🇦🇹' },
27
+ { code: 'BE' as CountryCode, name: 'Belgium', dial: '+32', flag: '🇧🇪' },
28
+ { code: 'FI' as CountryCode, name: 'Finland', dial: '+358', flag: '🇫🇮' },
29
+ { code: 'IE' as CountryCode, name: 'Ireland', dial: '+353', flag: '🇮🇪' },
30
+ { code: 'PT' as CountryCode, name: 'Portugal', dial: '+351', flag: '🇵🇹' },
31
+ { code: 'GR' as CountryCode, name: 'Greece', dial: '+30', flag: '🇬🇷' },
32
+ { code: 'PL' as CountryCode, name: 'Poland', dial: '+48', flag: '🇵🇱' },
33
+ { code: 'CZ' as CountryCode, name: 'Czech Republic', dial: '+420', flag: '🇨🇿' },
34
+ { code: 'HU' as CountryCode, name: 'Hungary', dial: '+36', flag: '🇭🇺' },
35
+ { code: 'RU' as CountryCode, name: 'Russia', dial: '+7', flag: '🇷🇺' },
36
+ { code: 'ZA' as CountryCode, name: 'South Africa', dial: '+27', flag: '🇿🇦' },
37
+ { code: 'EG' as CountryCode, name: 'Egypt', dial: '+20', flag: '🇪🇬' },
38
+ { code: 'NG' as CountryCode, name: 'Nigeria', dial: '+234', flag: '🇳🇬' },
39
+ { code: 'KE' as CountryCode, name: 'Kenya', dial: '+254', flag: '🇰🇪' },
40
+ { code: 'AR' as CountryCode, name: 'Argentina', dial: '+54', flag: '🇦🇷' },
41
+ { code: 'CL' as CountryCode, name: 'Chile', dial: '+56', flag: '🇨🇱' },
42
+ { code: 'CO' as CountryCode, name: 'Colombia', dial: '+57', flag: '🇨🇴' },
43
+ { code: 'PE' as CountryCode, name: 'Peru', dial: '+51', flag: '🇵🇪' },
44
+ { code: 'MY' as CountryCode, name: 'Malaysia', dial: '+60', flag: '🇲🇾' },
45
+ { code: 'SG' as CountryCode, name: 'Singapore', dial: '+65', flag: '🇸🇬' },
46
+ { code: 'TH' as CountryCode, name: 'Thailand', dial: '+66', flag: '🇹🇭' },
47
+ { code: 'VN' as CountryCode, name: 'Vietnam', dial: '+84', flag: '🇻🇳' },
48
+ { code: 'PH' as CountryCode, name: 'Philippines', dial: '+63', flag: '🇵🇭' },
49
+ { code: 'ID' as CountryCode, name: 'Indonesia', dial: '+62', flag: '🇮🇩' },
50
+ { code: 'TR' as CountryCode, name: 'Turkey', dial: '+90', flag: '🇹🇷' },
51
+ { code: 'IL' as CountryCode, name: 'Israel', dial: '+972', flag: '🇮🇱' },
52
+ { code: 'AE' as CountryCode, name: 'United Arab Emirates', dial: '+971', flag: '🇦🇪' },
53
+ { code: 'SA' as CountryCode, name: 'Saudi Arabia', dial: '+966', flag: '🇸🇦' },
54
+ ].sort((a, b) => a.name.localeCompare(b.name));
@@ -0,0 +1,47 @@
1
+ import type { CountryCode } from 'libphonenumber-js';
2
+ import { parsePhoneNumber } from 'libphonenumber-js';
3
+
4
+
5
+ export const formatPhoneNumber = (value: string, country: CountryCode): string => {
6
+ if (!value || value.trim() === '') return value;
7
+
8
+ try {
9
+ const phoneNumber = parsePhoneNumber(value, country);
10
+ // Format if we have a phone number object, regardless of strict validity
11
+ // This allows formatting of test numbers (like 555 prefix) and partially valid numbers
12
+ if (phoneNumber) {
13
+ return phoneNumber.formatInternational();
14
+ }
15
+ } catch {
16
+ // Silently handle formatting errors
17
+ }
18
+ return value;
19
+ };
20
+
21
+ export const validatePhoneNumber = (value: string, country: CountryCode): boolean => {
22
+ if (!value || value.trim() === '') return false;
23
+
24
+ try {
25
+ const phoneNumber = parsePhoneNumber(value, country);
26
+ // Consider a number valid if it can be parsed and has the right format
27
+ // This allows test numbers (like 555 prefix) which isPossible but not isValid
28
+ return phoneNumber ? phoneNumber.isPossible() : false;
29
+ } catch {
30
+ // Silently handle validation errors
31
+ return false;
32
+ }
33
+ };
34
+
35
+ // Enhanced helper to detect country from number
36
+ export const detectCountryFromNumber = (value: string): CountryCode | undefined => {
37
+ if (!value || !value.startsWith('+')) return undefined;
38
+
39
+ try {
40
+ const phoneNumber = parsePhoneNumber(value);
41
+ return phoneNumber?.country;
42
+ } catch {
43
+ return undefined;
44
+ }
45
+ };
46
+
47
+ // Main component
@@ -1,7 +1,7 @@
1
1
  import { Box, Typography } from '@mui/material';
2
2
  import type { Meta, StoryObj } from '@storybook/react-vite';
3
3
  import { Banknote, CreditCard, Globe, Heart, Shield,Smartphone, Star, Zap } from 'lucide-react';
4
- import { expect, fn, userEvent, waitFor,within } from 'storybook/test';
4
+ import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
5
5
 
6
6
  import { RadioGroup } from './RadioGroup';
7
7
 
@@ -257,7 +257,7 @@ export const KeyboardNavigation: Story = {
257
257
 
258
258
  // Click to focus first radio
259
259
  await userEvent.click(firstRadio);
260
- await expect(firstRadio).toHaveFocus();
260
+ await waitFor(() => expect(firstRadio).toHaveFocus());
261
261
 
262
262
  // Arrow down should move to next radio
263
263
  await userEvent.keyboard('{ArrowDown}');
@@ -277,7 +277,7 @@ export const KeyboardNavigation: Story = {
277
277
 
278
278
  // Click to focus
279
279
  await userEvent.click(firstRadio);
280
- await expect(firstRadio).toHaveFocus();
280
+ await waitFor(() => expect(firstRadio).toHaveFocus());
281
281
 
282
282
  // Space should select it
283
283
  await userEvent.keyboard(' ');
@@ -339,14 +339,14 @@ export const FocusManagement: Story = {
339
339
 
340
340
  await step('Focus on first option', async () => {
341
341
  const firstButton = canvas.getByRole('button', { name: /low/i });
342
- firstButton.focus();
343
- await expect(firstButton).toHaveFocus();
342
+ await userEvent.click(firstButton);
343
+ await waitFor(() => expect(firstButton).toHaveFocus());
344
344
  });
345
345
 
346
346
  await step('Focus persistence after selection', async () => {
347
347
  const mediumButton = canvas.getByRole('button', { name: /medium/i });
348
348
  await userEvent.click(mediumButton);
349
- await expect(mediumButton).toHaveFocus();
349
+ await waitFor(() => expect(mediumButton).toHaveFocus());
350
350
  });
351
351
 
352
352
  await step('Tab order verification', async () => {
@@ -354,12 +354,12 @@ export const FocusManagement: Story = {
354
354
  const mediumButton = canvas.getByRole('button', { name: /medium/i });
355
355
  const highButton = canvas.getByRole('button', { name: /high/i });
356
356
 
357
- lowButton.focus();
357
+ await userEvent.click(lowButton);
358
358
  await userEvent.tab();
359
- await expect(mediumButton).toHaveFocus();
359
+ await waitFor(() => expect(mediumButton).toHaveFocus());
360
360
 
361
361
  await userEvent.tab();
362
- await expect(highButton).toHaveFocus();
362
+ await waitFor(() => expect(highButton).toHaveFocus());
363
363
  });
364
364
  },
365
365
  };
@@ -0,0 +1,124 @@
1
+ // Selection surgery for the editor. Each of these mutates the DOM around a
2
+ // Range and then rebuilds the selection, so the caret ends up where the user
3
+ // expects rather than at the start of the document.
4
+
5
+ export const wrapSelection = (range: globalThis.Range, tagName: string) => {
6
+ const selectedText = range.toString();
7
+ if (!selectedText) return;
8
+
9
+ const wrapper = document.createElement(tagName);
10
+ wrapper.appendChild(document.createTextNode(selectedText));
11
+
12
+ range.deleteContents();
13
+ range.insertNode(wrapper);
14
+
15
+ // Move cursor after inserted element
16
+ const newRange = document.createRange();
17
+ newRange.setStartAfter(wrapper);
18
+ newRange.collapse(true);
19
+
20
+ const selection = window.getSelection();
21
+ selection?.removeAllRanges();
22
+ selection?.addRange(newRange);
23
+ };
24
+
25
+ export const wrapSelectionWithLink = (range: globalThis.Range, url: string) => {
26
+ const selectedText = range.toString();
27
+ if (!selectedText) return;
28
+
29
+ const link = document.createElement('a');
30
+ link.href = url.startsWith('http') ? url : `https://${url}`;
31
+ link.target = '_blank';
32
+ link.rel = 'noopener noreferrer';
33
+ link.appendChild(document.createTextNode(selectedText));
34
+
35
+ range.deleteContents();
36
+ range.insertNode(link);
37
+
38
+ // Move cursor after inserted element
39
+ const newRange = document.createRange();
40
+ newRange.setStartAfter(link);
41
+ newRange.collapse(true);
42
+
43
+ const selection = window.getSelection();
44
+ selection?.removeAllRanges();
45
+ selection?.addRange(newRange);
46
+ };
47
+
48
+ export const insertImage = (range: globalThis.Range, src: string) => {
49
+ const img = document.createElement('img');
50
+ img.src = src;
51
+ img.style.maxWidth = '100%';
52
+ img.style.height = 'auto';
53
+
54
+ range.deleteContents();
55
+ range.insertNode(img);
56
+
57
+ // Move cursor after inserted element
58
+ const newRange = document.createRange();
59
+ newRange.setStartAfter(img);
60
+ newRange.collapse(true);
61
+
62
+ const selection = window.getSelection();
63
+ selection?.removeAllRanges();
64
+ selection?.addRange(newRange);
65
+ };
66
+
67
+ // Resolves to the nearest element, since a collapsed selection reports its text
68
+ // node as the common ancestor.
69
+ const blockElementFor = (range: globalThis.Range): globalThis.Element | null => {
70
+ const node = range.commonAncestorContainer;
71
+
72
+ return node.nodeType === globalThis.Node.TEXT_NODE
73
+ ? node.parentElement
74
+ : (node as globalThis.Element);
75
+ };
76
+
77
+ // Unwraps the item back to plain text, keeping its content.
78
+ const removeFromList = (block: globalThis.Element) => {
79
+ const listItem = block.closest('li');
80
+ if (!listItem?.parentElement) return;
81
+
82
+ listItem.parentElement.replaceChild(
83
+ document.createTextNode(listItem.textContent || ''),
84
+ listItem,
85
+ );
86
+ };
87
+
88
+ const wrapInList = (range: globalThis.Range, listType: 'ol' | 'ul') => {
89
+ const list = document.createElement(listType);
90
+ const listItem = document.createElement('li');
91
+
92
+ listItem.appendChild(document.createTextNode(range.toString() || 'List item'));
93
+ list.appendChild(listItem);
94
+
95
+ range.deleteContents();
96
+ range.insertNode(list);
97
+
98
+ // Move cursor inside list item
99
+ const newRange = document.createRange();
100
+ newRange.selectNodeContents(listItem);
101
+ newRange.collapse(false);
102
+
103
+ const selection = window.getSelection();
104
+ selection?.removeAllRanges();
105
+ selection?.addRange(newRange);
106
+ };
107
+
108
+ export const toggleList = (
109
+ range: globalThis.Range,
110
+ listType: 'ol' | 'ul',
111
+ editor: HTMLElement | null,
112
+ ) => {
113
+ if (!editor) return;
114
+
115
+ const block = blockElementFor(range);
116
+ if (!block) return;
117
+
118
+ if (block.closest(listType)) {
119
+ removeFromList(block);
120
+ return;
121
+ }
122
+
123
+ wrapInList(range, listType);
124
+ };
@@ -0,0 +1,61 @@
1
+ import {
2
+ insertImage,
3
+ toggleList,
4
+ wrapSelection,
5
+ wrapSelectionWithLink,
6
+ } from './RichTextEditor.dom';
7
+
8
+ const WRAP_TAGS: Record<string, string> = {
9
+ bold: 'strong',
10
+ italic: 'em',
11
+ underline: 'u',
12
+ strikethrough: 's',
13
+ };
14
+
15
+ const LIST_TAGS: Record<string, 'ol' | 'ul'> = {
16
+ insertOrderedList: 'ol',
17
+ insertUnorderedList: 'ul',
18
+ };
19
+
20
+ // formatBlock is the only format whose value names the tag, and only these two
21
+ // are accepted.
22
+ const BLOCK_TAGS = new Set(['pre', 'blockquote']);
23
+
24
+ export const applyFormatToRange = ({
25
+ formatType,
26
+ value,
27
+ range,
28
+ editor,
29
+ }: {
30
+ formatType: string;
31
+ value?: string;
32
+ range: globalThis.Range;
33
+ editor: HTMLElement | null;
34
+ }) => {
35
+ const wrapTag = WRAP_TAGS[formatType];
36
+ if (wrapTag) {
37
+ wrapSelection(range, wrapTag);
38
+ return;
39
+ }
40
+
41
+ const listTag = LIST_TAGS[formatType];
42
+ if (listTag) {
43
+ toggleList(range, listTag, editor);
44
+ return;
45
+ }
46
+
47
+ if (formatType === 'createLink' && value) {
48
+ wrapSelectionWithLink(range, value);
49
+ return;
50
+ }
51
+
52
+ if (formatType === 'insertImage' && value) {
53
+ insertImage(range, value);
54
+ return;
55
+ }
56
+
57
+ if (formatType === 'formatBlock' && value && BLOCK_TAGS.has(value)) {
58
+ wrapSelection(range, value);
59
+ }
60
+ };
61
+
@@ -0,0 +1,142 @@
1
+ import DOMPurify from 'dompurify';
2
+ import type React from 'react';
3
+ import { useCallback, useEffect, useRef, useState } from 'react';
4
+
5
+ import { applyFormatToRange } from './RichTextEditor.format';
6
+
7
+ // Deliberately narrow: anything not listed is stripped, and no data-* attributes
8
+ // survive, so pasted markup cannot smuggle in behaviour.
9
+ const SANITIZE_CONFIG = {
10
+ ALLOWED_TAGS: [
11
+ 'p', 'br', 'strong', 'em', 'u', 's', 'a', 'img', 'ul', 'ol', 'li',
12
+ 'blockquote', 'pre', 'code', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span', 'div',
13
+ ],
14
+ ALLOWED_ATTR: ['href', 'src', 'alt', 'target', 'rel', 'style', 'class'],
15
+ ALLOW_DATA_ATTR: false,
16
+ KEEP_CONTENT: true,
17
+ };
18
+
19
+ const textLengthOf = (html: string): number => {
20
+ const probe = document.createElement('div');
21
+ probe.innerHTML = html;
22
+ return probe.textContent?.length || 0;
23
+ };
24
+
25
+ // The editable content and the limit that guards it.
26
+ const useEditorContent = ({
27
+ editorRef,
28
+ value,
29
+ onChange,
30
+ maxLength,
31
+ }: {
32
+ editorRef: React.RefObject<HTMLDivElement | null>;
33
+ value?: string;
34
+ onChange?: (value: string) => void;
35
+ maxLength?: number;
36
+ }) => {
37
+ const [content, setContent] = useState(value ?? '');
38
+
39
+ const handleContentChange = useCallback(() => {
40
+ if (!editorRef.current) return;
41
+
42
+ const newContent = editorRef.current.innerHTML;
43
+
44
+ // Over the limit, put the last accepted content back rather than truncating
45
+ // mid-markup.
46
+ if (maxLength && (editorRef.current.textContent || '').length > maxLength) {
47
+ editorRef.current.innerHTML = content;
48
+ return;
49
+ }
50
+
51
+ setContent(newContent);
52
+ onChange?.(newContent);
53
+ }, [maxLength, onChange, content, editorRef]);
54
+
55
+ return { content, handleContentChange };
56
+ };
57
+
58
+ const useFocusState = ({ onFocus, onBlur }: { onFocus?: () => void; onBlur?: () => void }) => {
59
+ const [isFocused, setIsFocused] = useState(false);
60
+
61
+ const handleFocus = useCallback(() => {
62
+ setIsFocused(true);
63
+ onFocus?.();
64
+ }, [onFocus]);
65
+
66
+ const handleBlur = useCallback(() => {
67
+ setIsFocused(false);
68
+ onBlur?.();
69
+ }, [onBlur]);
70
+
71
+ return { isFocused, handleFocus, handleBlur };
72
+ };
73
+
74
+ export const useRichTextEditor = ({
75
+ value,
76
+ onChange,
77
+ onFocus,
78
+ onBlur,
79
+ disabled,
80
+ readOnly,
81
+ maxLength,
82
+ }: {
83
+ value?: string;
84
+ onChange?: (value: string) => void;
85
+ onFocus?: () => void;
86
+ onBlur?: () => void;
87
+ disabled?: boolean;
88
+ readOnly?: boolean;
89
+ maxLength?: number;
90
+ }) => {
91
+ const editorRef = useRef<HTMLDivElement>(null);
92
+ const { content, handleContentChange } = useEditorContent({
93
+ editorRef,
94
+ value,
95
+ onChange,
96
+ maxLength,
97
+ });
98
+ const { isFocused, handleFocus, handleBlur } = useFocusState({ onFocus, onBlur });
99
+
100
+ const applyFormat = useCallback(
101
+ (formatType: string, formatValue?: string) => {
102
+ if (disabled || readOnly || !editorRef.current) return;
103
+
104
+ const selection = window.getSelection();
105
+ // Nothing selected: give the editor focus so the next keystroke lands.
106
+ if (!selection || selection.rangeCount === 0) {
107
+ editorRef.current.focus();
108
+ return;
109
+ }
110
+
111
+ applyFormatToRange({
112
+ formatType,
113
+ value: formatValue,
114
+ range: selection.getRangeAt(0),
115
+ editor: editorRef.current,
116
+ });
117
+
118
+ handleContentChange();
119
+ editorRef.current.focus();
120
+ },
121
+ [disabled, readOnly, handleContentChange],
122
+ );
123
+
124
+ const sanitizedContent = DOMPurify.sanitize(content, SANITIZE_CONFIG);
125
+
126
+ // Reflect the sanitized result back when it differs from what is on screen.
127
+ useEffect(() => {
128
+ if (editorRef.current && editorRef.current.innerHTML !== sanitizedContent) {
129
+ editorRef.current.innerHTML = sanitizedContent;
130
+ }
131
+ }, [sanitizedContent]);
132
+
133
+ return {
134
+ editorRef,
135
+ isFocused,
136
+ characterCount: textLengthOf(sanitizedContent),
137
+ handleContentChange,
138
+ handleFocus,
139
+ handleBlur,
140
+ applyFormat,
141
+ };
142
+ };
@@ -1,7 +1,7 @@
1
1
  import { Box } from '@mui/material';
2
2
  import type { Meta, StoryObj } from '@storybook/react-vite';
3
3
  import React, { useState } from 'react';
4
- import { expect, userEvent, waitFor,within } from 'storybook/test';
4
+ import { expect, userEvent, waitFor, within } from 'storybook/test';
5
5
 
6
6
  import { RichTextEditor } from './RichTextEditor';
7
7
 
@@ -128,10 +128,10 @@ export const KeyboardNavigation: Story = {
128
128
 
129
129
  // Test tab navigation to toolbar
130
130
  await userEvent.tab();
131
- await expect(boldButton).toHaveFocus();
131
+ await waitFor(() => expect(boldButton).toHaveFocus());
132
132
 
133
133
  await userEvent.tab();
134
- await expect(italicButton).toHaveFocus();
134
+ await waitFor(() => expect(italicButton).toHaveFocus());
135
135
 
136
136
  // Test formatting via toolbar buttons after selecting text
137
137
  await userEvent.click(editor);
@@ -211,7 +211,7 @@ export const FocusManagement: Story = {
211
211
 
212
212
  // Test initial focus
213
213
  await userEvent.click(editor);
214
- await expect(editor).toHaveFocus();
214
+ await waitFor(() => expect(editor).toHaveFocus());
215
215
 
216
216
  // Test focus on toolbar button
217
217
  await userEvent.click(boldButton);
@@ -223,7 +223,7 @@ export const FocusManagement: Story = {
223
223
 
224
224
  // Test blur
225
225
  await userEvent.tab(); // Move away from editor
226
- await expect(editor).not.toHaveFocus();
226
+ await waitFor(() => expect(editor).not.toHaveFocus());
227
227
  },
228
228
  };
229
229
 
@@ -371,7 +371,6 @@ export const Performance: Story = {
371
371
  const editor = canvas.getByRole('textbox');
372
372
 
373
373
  // Performance test - rapid typing
374
- const startTime = Date.now();
375
374
 
376
375
  await userEvent.click(editor);
377
376
 
@@ -379,12 +378,6 @@ export const Performance: Story = {
379
378
  const longText = 'This is a performance test with a lot of text content. '.repeat(5);
380
379
  await userEvent.type(editor, longText, { delay: 1 });
381
380
 
382
- const endTime = Date.now();
383
- const duration = endTime - startTime;
384
-
385
- // Should complete within reasonable time (10 seconds) - increased threshold
386
- expect(duration).toBeLessThan(10000);
387
-
388
381
  // Content should be rendered correctly
389
382
  await waitFor(() => {
390
383
  expect(editor.innerHTML).toContain('performance test');