@elabs-ai/components-ui 4.0.0

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 (449) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +78 -0
  3. package/dist/index.d.ts +4497 -0
  4. package/dist/index.js +15192 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/lib/cn.d.ts +10 -0
  7. package/dist/lib/cn.js +20 -0
  8. package/dist/lib/cn.js.map +1 -0
  9. package/package.json +106 -0
  10. package/src/blocks/sidebar-02/app-sidebar.tsx +160 -0
  11. package/src/blocks/sidebar-02/logo.tsx +18 -0
  12. package/src/blocks/sidebar-02/nav-main.tsx +13 -0
  13. package/src/blocks/sidebar-02/nav-notifications.tsx +7 -0
  14. package/src/blocks/sidebar-02/sidebar-02.stories.tsx +23 -0
  15. package/src/blocks/sidebar-02/team-switcher.tsx +7 -0
  16. package/src/blocks/sidebar-04/app-sidebar.tsx +236 -0
  17. package/src/blocks/sidebar-04/mail-context.tsx +29 -0
  18. package/src/blocks/sidebar-04/nav-user.tsx +6 -0
  19. package/src/blocks/sidebar-04/sidebar-04.stories.tsx +150 -0
  20. package/src/blocks/sidebar-05/app-sidebar.tsx +375 -0
  21. package/src/blocks/sidebar-05/sidebar-05.stories.tsx +18 -0
  22. package/src/blocks/sidebar-05/team-switcher.tsx +6 -0
  23. package/src/blocks-comparison-table.stories.tsx +165 -0
  24. package/src/components/accordion/accordion.stories.tsx +71 -0
  25. package/src/components/accordion/accordion.test.tsx +18 -0
  26. package/src/components/accordion/accordion.tsx +49 -0
  27. package/src/components/accordion/index.ts +1 -0
  28. package/src/components/advanced-group/advanced-group.stories.tsx +143 -0
  29. package/src/components/advanced-group/advanced-group.test.tsx +81 -0
  30. package/src/components/advanced-group/advanced-group.tsx +111 -0
  31. package/src/components/advanced-group/index.ts +2 -0
  32. package/src/components/alert/alert.stories.tsx +156 -0
  33. package/src/components/alert/alert.test.tsx +57 -0
  34. package/src/components/alert/alert.tsx +91 -0
  35. package/src/components/alert/index.ts +8 -0
  36. package/src/components/alert-dialog/alert-dialog.stories.tsx +73 -0
  37. package/src/components/alert-dialog/alert-dialog.tsx +104 -0
  38. package/src/components/alert-dialog/index.ts +12 -0
  39. package/src/components/app-shell/app-shell.stories.tsx +61 -0
  40. package/src/components/app-shell/app-shell.tsx +31 -0
  41. package/src/components/app-shell/index.ts +1 -0
  42. package/src/components/app-sidebar/app-sidebar.stories.tsx +121 -0
  43. package/src/components/app-sidebar/app-sidebar.test.tsx +55 -0
  44. package/src/components/app-sidebar/app-sidebar.tsx +53 -0
  45. package/src/components/app-sidebar/index.ts +1 -0
  46. package/src/components/aspect-ratio/aspect-ratio.stories.tsx +46 -0
  47. package/src/components/aspect-ratio/aspect-ratio.tsx +3 -0
  48. package/src/components/aspect-ratio/index.ts +1 -0
  49. package/src/components/attribution-panel/attribution-panel.stories.tsx +83 -0
  50. package/src/components/attribution-panel/attribution-panel.test.tsx +123 -0
  51. package/src/components/attribution-panel/attribution-panel.tsx +191 -0
  52. package/src/components/attribution-panel/attribution-types.ts +44 -0
  53. package/src/components/attribution-panel/attributions.generated.ts +1422 -0
  54. package/src/components/attribution-panel/index.ts +3 -0
  55. package/src/components/avatar/avatar.stories.tsx +45 -0
  56. package/src/components/avatar/avatar.test.tsx +13 -0
  57. package/src/components/avatar/avatar.tsx +46 -0
  58. package/src/components/avatar/index.ts +1 -0
  59. package/src/components/badge/badge.stories.tsx +44 -0
  60. package/src/components/badge/badge.test.tsx +10 -0
  61. package/src/components/badge/badge.tsx +28 -0
  62. package/src/components/badge/index.ts +1 -0
  63. package/src/components/bento-grid/bento-grid.stories.tsx +165 -0
  64. package/src/components/bento-grid/bento-grid.test.tsx +119 -0
  65. package/src/components/bento-grid/bento-grid.tsx +339 -0
  66. package/src/components/bento-grid/index.ts +9 -0
  67. package/src/components/bounded-number/bounded-number.stories.tsx +118 -0
  68. package/src/components/bounded-number/bounded-number.test.tsx +73 -0
  69. package/src/components/bounded-number/bounded-number.tsx +110 -0
  70. package/src/components/bounded-number/index.ts +1 -0
  71. package/src/components/breadcrumb/breadcrumb.stories.tsx +47 -0
  72. package/src/components/breadcrumb/breadcrumb.test.tsx +17 -0
  73. package/src/components/breadcrumb/breadcrumb.tsx +86 -0
  74. package/src/components/breadcrumb/index.ts +9 -0
  75. package/src/components/button/button.stories.tsx +70 -0
  76. package/src/components/button/button.test.tsx +50 -0
  77. package/src/components/button/button.tsx +63 -0
  78. package/src/components/button/index.ts +1 -0
  79. package/src/components/button-group/button-group.stories.tsx +45 -0
  80. package/src/components/button-group/button-group.tsx +70 -0
  81. package/src/components/button-group/index.ts +6 -0
  82. package/src/components/calendar/calendar.stories.tsx +95 -0
  83. package/src/components/calendar/calendar.test.tsx +140 -0
  84. package/src/components/calendar/calendar.tsx +70 -0
  85. package/src/components/calendar/index.ts +1 -0
  86. package/src/components/card/card.stories.tsx +438 -0
  87. package/src/components/card/card.test.tsx +153 -0
  88. package/src/components/card/card.tsx +332 -0
  89. package/src/components/card/index.ts +13 -0
  90. package/src/components/carousel/carousel.stories.tsx +102 -0
  91. package/src/components/carousel/carousel.tsx +225 -0
  92. package/src/components/carousel/index.ts +8 -0
  93. package/src/components/change-review/change-review.stories.tsx +288 -0
  94. package/src/components/change-review/change-review.test.tsx +261 -0
  95. package/src/components/change-review/change-review.tsx +637 -0
  96. package/src/components/change-review/index.ts +23 -0
  97. package/src/components/checkbox/checkbox.stories.tsx +119 -0
  98. package/src/components/checkbox/checkbox.test.tsx +40 -0
  99. package/src/components/checkbox/checkbox.tsx +34 -0
  100. package/src/components/checkbox/index.ts +1 -0
  101. package/src/components/collapsible/collapsible.stories.tsx +62 -0
  102. package/src/components/collapsible/collapsible.tsx +22 -0
  103. package/src/components/collapsible/index.ts +1 -0
  104. package/src/components/collapsible-panel/index.ts +8 -0
  105. package/src/components/collapsible-panel/use-collapsible-panel.test.tsx +93 -0
  106. package/src/components/collapsible-panel/use-collapsible-panel.ts +159 -0
  107. package/src/components/color-picker/color-picker.stories.tsx +93 -0
  108. package/src/components/color-picker/color-picker.test.tsx +79 -0
  109. package/src/components/color-picker/color-picker.tsx +341 -0
  110. package/src/components/color-picker/index.ts +1 -0
  111. package/src/components/combobox/combobox.stories.tsx +154 -0
  112. package/src/components/combobox/combobox.test.tsx +246 -0
  113. package/src/components/combobox/combobox.tsx +210 -0
  114. package/src/components/combobox/index.ts +1 -0
  115. package/src/components/command/command.stories.tsx +371 -0
  116. package/src/components/command/command.test.tsx +280 -0
  117. package/src/components/command/command.tsx +301 -0
  118. package/src/components/command/index.ts +13 -0
  119. package/src/components/confirm-dialog/confirm-dialog.stories.tsx +183 -0
  120. package/src/components/confirm-dialog/confirm-dialog.test.tsx +147 -0
  121. package/src/components/confirm-dialog/confirm-dialog.tsx +146 -0
  122. package/src/components/confirm-dialog/index.ts +2 -0
  123. package/src/components/context-menu/context-menu.stories.tsx +70 -0
  124. package/src/components/context-menu/context-menu.tsx +172 -0
  125. package/src/components/context-menu/index.ts +16 -0
  126. package/src/components/copyable-value/copyable-value.stories.tsx +80 -0
  127. package/src/components/copyable-value/copyable-value.test.tsx +128 -0
  128. package/src/components/copyable-value/copyable-value.tsx +101 -0
  129. package/src/components/copyable-value/index.ts +1 -0
  130. package/src/components/date-picker/date-picker.stories.tsx +42 -0
  131. package/src/components/date-picker/date-picker.tsx +49 -0
  132. package/src/components/date-picker/index.ts +1 -0
  133. package/src/components/date-range-picker/date-range-picker.stories.tsx +151 -0
  134. package/src/components/date-range-picker/date-range-picker.test.tsx +62 -0
  135. package/src/components/date-range-picker/date-range-picker.tsx +126 -0
  136. package/src/components/date-range-picker/index.ts +6 -0
  137. package/src/components/descriptions/descriptions.stories.tsx +43 -0
  138. package/src/components/descriptions/descriptions.test.tsx +41 -0
  139. package/src/components/descriptions/descriptions.tsx +95 -0
  140. package/src/components/descriptions/index.ts +6 -0
  141. package/src/components/dialog/dialog.stories.tsx +728 -0
  142. package/src/components/dialog/dialog.test.tsx +285 -0
  143. package/src/components/dialog/dialog.tsx +340 -0
  144. package/src/components/dialog/index.ts +18 -0
  145. package/src/components/dialog/use-dialog-dismiss-guard.test.tsx +80 -0
  146. package/src/components/dialog/use-dialog-dismiss-guard.ts +109 -0
  147. package/src/components/drawer/drawer.stories.tsx +76 -0
  148. package/src/components/drawer/drawer.tsx +85 -0
  149. package/src/components/drawer/index.ts +11 -0
  150. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +176 -0
  151. package/src/components/dropdown-menu/dropdown-menu.test.tsx +109 -0
  152. package/src/components/dropdown-menu/dropdown-menu.tsx +179 -0
  153. package/src/components/dropdown-menu/index.ts +17 -0
  154. package/src/components/empty-state/empty-state.stories.tsx +54 -0
  155. package/src/components/empty-state/empty-state.test.tsx +11 -0
  156. package/src/components/empty-state/empty-state.tsx +30 -0
  157. package/src/components/empty-state/index.ts +1 -0
  158. package/src/components/error-state/error-state.stories.tsx +51 -0
  159. package/src/components/error-state/error-state.tsx +27 -0
  160. package/src/components/error-state/index.ts +1 -0
  161. package/src/components/expand-dialog/expand-dialog.stories.tsx +246 -0
  162. package/src/components/expand-dialog/expand-dialog.test.tsx +185 -0
  163. package/src/components/expand-dialog/expand-dialog.tsx +387 -0
  164. package/src/components/expand-dialog/index.ts +11 -0
  165. package/src/components/field-row/field-row.stories.tsx +114 -0
  166. package/src/components/field-row/field-row.test.tsx +188 -0
  167. package/src/components/field-row/field-row.tsx +115 -0
  168. package/src/components/field-row/index.ts +1 -0
  169. package/src/components/file-upload/file-upload.stories.tsx +183 -0
  170. package/src/components/file-upload/file-upload.test.tsx +169 -0
  171. package/src/components/file-upload/file-upload.tsx +459 -0
  172. package/src/components/file-upload/index.ts +13 -0
  173. package/src/components/form/form.stories.tsx +71 -0
  174. package/src/components/form/form.tsx +141 -0
  175. package/src/components/form/index.ts +10 -0
  176. package/src/components/hover-card/hover-card.stories.tsx +57 -0
  177. package/src/components/hover-card/hover-card.tsx +25 -0
  178. package/src/components/hover-card/index.ts +1 -0
  179. package/src/components/icon-button/icon-button.stories.tsx +108 -0
  180. package/src/components/icon-button/icon-button.test.tsx +93 -0
  181. package/src/components/icon-button/icon-button.tsx +139 -0
  182. package/src/components/icon-button/index.ts +1 -0
  183. package/src/components/input/index.ts +1 -0
  184. package/src/components/input/input.stories.tsx +44 -0
  185. package/src/components/input/input.test.tsx +19 -0
  186. package/src/components/input/input.tsx +26 -0
  187. package/src/components/input-group/index.ts +11 -0
  188. package/src/components/input-group/input-group.stories.tsx +87 -0
  189. package/src/components/input-group/input-group.test.tsx +48 -0
  190. package/src/components/input-group/input-group.tsx +170 -0
  191. package/src/components/input-otp/index.ts +1 -0
  192. package/src/components/input-otp/input-otp.stories.tsx +66 -0
  193. package/src/components/input-otp/input-otp.tsx +68 -0
  194. package/src/components/kbd/index.ts +1 -0
  195. package/src/components/kbd/kbd.stories.tsx +45 -0
  196. package/src/components/kbd/kbd.tsx +20 -0
  197. package/src/components/key-value-editor/index.ts +1 -0
  198. package/src/components/key-value-editor/key-value-editor.stories.tsx +130 -0
  199. package/src/components/key-value-editor/key-value-editor.test.tsx +231 -0
  200. package/src/components/key-value-editor/key-value-editor.tsx +263 -0
  201. package/src/components/label/index.ts +1 -0
  202. package/src/components/label/label.stories.tsx +35 -0
  203. package/src/components/label/label.tsx +19 -0
  204. package/src/components/link-preview/index.ts +2 -0
  205. package/src/components/link-preview/link-preview.stories.tsx +178 -0
  206. package/src/components/link-preview/link-preview.test.tsx +123 -0
  207. package/src/components/link-preview/link-preview.tsx +194 -0
  208. package/src/components/list-editor/index.ts +1 -0
  209. package/src/components/list-editor/list-editor.stories.tsx +119 -0
  210. package/src/components/list-editor/list-editor.test.tsx +96 -0
  211. package/src/components/list-editor/list-editor.tsx +192 -0
  212. package/src/components/loading-state/index.ts +1 -0
  213. package/src/components/loading-state/loading-state.stories.tsx +38 -0
  214. package/src/components/loading-state/loading-state.tsx +26 -0
  215. package/src/components/locale-provider/index.ts +8 -0
  216. package/src/components/locale-provider/locale-provider.stories.tsx +83 -0
  217. package/src/components/locale-provider/locale-provider.test.tsx +238 -0
  218. package/src/components/locale-provider/locale-provider.tsx +187 -0
  219. package/src/components/locale-provider/messages.ts +300 -0
  220. package/src/components/match-highlight/index.ts +7 -0
  221. package/src/components/match-highlight/match-highlight.stories.tsx +146 -0
  222. package/src/components/match-highlight/match-highlight.test.tsx +202 -0
  223. package/src/components/match-highlight/match-highlight.tsx +145 -0
  224. package/src/components/mention-input/index.ts +28 -0
  225. package/src/components/mention-input/mention-input-mirror.stories.tsx +171 -0
  226. package/src/components/mention-input/mention-input.stories.tsx +463 -0
  227. package/src/components/mention-input/mention-input.test.tsx +617 -0
  228. package/src/components/mention-input/mention-input.tsx +1102 -0
  229. package/src/components/mention-input/mention-value.test.ts +253 -0
  230. package/src/components/mention-input/mention-value.ts +193 -0
  231. package/src/components/menubar/index.ts +17 -0
  232. package/src/components/menubar/menubar.stories.tsx +67 -0
  233. package/src/components/menubar/menubar.tsx +198 -0
  234. package/src/components/metric-card/index.ts +1 -0
  235. package/src/components/metric-card/metric-card.stories.tsx +224 -0
  236. package/src/components/metric-card/metric-card.test.tsx +198 -0
  237. package/src/components/metric-card/metric-card.tsx +229 -0
  238. package/src/components/model-picker/index.ts +11 -0
  239. package/src/components/model-picker/model-picker-state.ts +85 -0
  240. package/src/components/model-picker/model-picker.stories.tsx +128 -0
  241. package/src/components/model-picker/model-picker.test.tsx +241 -0
  242. package/src/components/model-picker/model-picker.tsx +270 -0
  243. package/src/components/nav-main/index.ts +1 -0
  244. package/src/components/nav-main/nav-main.tsx +147 -0
  245. package/src/components/nav-notifications/index.ts +5 -0
  246. package/src/components/nav-notifications/nav-notifications.test.tsx +15 -0
  247. package/src/components/nav-notifications/nav-notifications.tsx +70 -0
  248. package/src/components/nav-user/index.ts +1 -0
  249. package/src/components/nav-user/nav-user.test.tsx +29 -0
  250. package/src/components/nav-user/nav-user.tsx +114 -0
  251. package/src/components/navigation-menu/index.ts +10 -0
  252. package/src/components/navigation-menu/navigation-menu.stories.tsx +92 -0
  253. package/src/components/navigation-menu/navigation-menu.tsx +99 -0
  254. package/src/components/number-input/index.ts +1 -0
  255. package/src/components/number-input/number-input.stories.tsx +120 -0
  256. package/src/components/number-input/number-input.test.tsx +96 -0
  257. package/src/components/number-input/number-input.tsx +219 -0
  258. package/src/components/page-shell/index.ts +1 -0
  259. package/src/components/page-shell/page-shell.stories.tsx +105 -0
  260. package/src/components/page-shell/page-shell.test.tsx +57 -0
  261. package/src/components/page-shell/page-shell.tsx +62 -0
  262. package/src/components/pagination/index.ts +9 -0
  263. package/src/components/pagination/pagination.stories.tsx +57 -0
  264. package/src/components/pagination/pagination.tsx +77 -0
  265. package/src/components/popover/index.ts +1 -0
  266. package/src/components/popover/popover.stories.tsx +28 -0
  267. package/src/components/popover/popover.test.tsx +16 -0
  268. package/src/components/popover/popover.tsx +29 -0
  269. package/src/components/progress/index.ts +1 -0
  270. package/src/components/progress/progress.stories.tsx +80 -0
  271. package/src/components/progress/progress.test.tsx +35 -0
  272. package/src/components/progress/progress.tsx +50 -0
  273. package/src/components/radio-group/index.ts +1 -0
  274. package/src/components/radio-group/radio-group.stories.tsx +75 -0
  275. package/src/components/radio-group/radio-group.test.tsx +16 -0
  276. package/src/components/radio-group/radio-group.tsx +33 -0
  277. package/src/components/rating/index.ts +1 -0
  278. package/src/components/rating/rating.stories.tsx +111 -0
  279. package/src/components/rating/rating.test.tsx +41 -0
  280. package/src/components/rating/rating.tsx +226 -0
  281. package/src/components/resizable/index.ts +1 -0
  282. package/src/components/resizable/resizable.stories.tsx +25 -0
  283. package/src/components/resizable/resizable.tsx +40 -0
  284. package/src/components/reveal/index.ts +7 -0
  285. package/src/components/reveal/reveal.stories.tsx +90 -0
  286. package/src/components/reveal/reveal.test.tsx +43 -0
  287. package/src/components/reveal/reveal.tsx +95 -0
  288. package/src/components/revision-timeline/index.ts +10 -0
  289. package/src/components/revision-timeline/revision-timeline.stories.tsx +785 -0
  290. package/src/components/revision-timeline/revision-timeline.test.tsx +509 -0
  291. package/src/components/revision-timeline/revision-timeline.tsx +1107 -0
  292. package/src/components/scroll-area/index.ts +1 -0
  293. package/src/components/scroll-area/scroll-area.stories.tsx +45 -0
  294. package/src/components/scroll-area/scroll-area.tsx +43 -0
  295. package/src/components/section-header/index.ts +1 -0
  296. package/src/components/section-header/section-header.stories.tsx +20 -0
  297. package/src/components/section-header/section-header.tsx +36 -0
  298. package/src/components/segmented-field/index.ts +5 -0
  299. package/src/components/segmented-field/segmented-field.stories.tsx +121 -0
  300. package/src/components/segmented-field/segmented-field.test.tsx +163 -0
  301. package/src/components/segmented-field/segmented-field.tsx +235 -0
  302. package/src/components/select/index.ts +10 -0
  303. package/src/components/select/select.stories.tsx +106 -0
  304. package/src/components/select/select.test.tsx +98 -0
  305. package/src/components/select/select.tsx +230 -0
  306. package/src/components/separator/index.ts +2 -0
  307. package/src/components/separator/separator.stories.tsx +87 -0
  308. package/src/components/separator/separator.tsx +51 -0
  309. package/src/components/sheet/index.ts +12 -0
  310. package/src/components/sheet/sheet.stories.tsx +65 -0
  311. package/src/components/sheet/sheet.tsx +109 -0
  312. package/src/components/sidebar/index.ts +27 -0
  313. package/src/components/sidebar/sidebar.stories.tsx +69 -0
  314. package/src/components/sidebar/sidebar.test.tsx +43 -0
  315. package/src/components/sidebar/sidebar.tsx +688 -0
  316. package/src/components/skeleton/index.ts +1 -0
  317. package/src/components/skeleton/skeleton.stories.tsx +28 -0
  318. package/src/components/skeleton/skeleton.tsx +15 -0
  319. package/src/components/slider/index.ts +1 -0
  320. package/src/components/slider/slider.stories.tsx +211 -0
  321. package/src/components/slider/slider.test.tsx +168 -0
  322. package/src/components/slider/slider.tsx +136 -0
  323. package/src/components/slider-number/index.ts +1 -0
  324. package/src/components/slider-number/slider-number.stories.tsx +123 -0
  325. package/src/components/slider-number/slider-number.test.tsx +165 -0
  326. package/src/components/slider-number/slider-number.tsx +191 -0
  327. package/src/components/sonner/index.ts +1 -0
  328. package/src/components/sonner/sonner.stories.tsx +124 -0
  329. package/src/components/sonner/sonner.test.tsx +76 -0
  330. package/src/components/sonner/sonner.tsx +101 -0
  331. package/src/components/spinner/index.ts +1 -0
  332. package/src/components/spinner/spinner.stories.tsx +27 -0
  333. package/src/components/spinner/spinner.tsx +19 -0
  334. package/src/components/split-panel/index.ts +1 -0
  335. package/src/components/split-panel/split-panel.stories.tsx +60 -0
  336. package/src/components/split-panel/split-panel.test.tsx +38 -0
  337. package/src/components/split-panel/split-panel.tsx +95 -0
  338. package/src/components/state-panel/index.ts +1 -0
  339. package/src/components/state-panel/state-panel.stories.tsx +80 -0
  340. package/src/components/state-panel/state-panel.test.tsx +64 -0
  341. package/src/components/state-panel/state-panel.tsx +167 -0
  342. package/src/components/status-badge/index.ts +18 -0
  343. package/src/components/status-badge/status-badge.stories.tsx +132 -0
  344. package/src/components/status-badge/status-badge.test.tsx +189 -0
  345. package/src/components/status-badge/status-badge.tsx +330 -0
  346. package/src/components/switch/index.ts +1 -0
  347. package/src/components/switch/switch.stories.tsx +58 -0
  348. package/src/components/switch/switch.test.tsx +12 -0
  349. package/src/components/switch/switch.tsx +29 -0
  350. package/src/components/table/index.ts +10 -0
  351. package/src/components/table/table.stories.tsx +53 -0
  352. package/src/components/table/table.test.tsx +17 -0
  353. package/src/components/table/table.tsx +106 -0
  354. package/src/components/tabs/index.ts +1 -0
  355. package/src/components/tabs/tabs.stories.tsx +267 -0
  356. package/src/components/tabs/tabs.test.tsx +94 -0
  357. package/src/components/tabs/tabs.tsx +170 -0
  358. package/src/components/tag-input/index.ts +1 -0
  359. package/src/components/tag-input/tag-input.stories.tsx +120 -0
  360. package/src/components/tag-input/tag-input.test.tsx +97 -0
  361. package/src/components/tag-input/tag-input.tsx +253 -0
  362. package/src/components/team-switcher/index.ts +1 -0
  363. package/src/components/team-switcher/team-switcher.test.tsx +39 -0
  364. package/src/components/team-switcher/team-switcher.tsx +100 -0
  365. package/src/components/textarea/index.ts +1 -0
  366. package/src/components/textarea/textarea.stories.tsx +42 -0
  367. package/src/components/textarea/textarea.tsx +24 -0
  368. package/src/components/theme-switcher/index.ts +2 -0
  369. package/src/components/theme-switcher/theme-switcher.stories.tsx +146 -0
  370. package/src/components/theme-switcher/theme-switcher.test.tsx +253 -0
  371. package/src/components/theme-switcher/theme-switcher.tsx +280 -0
  372. package/src/components/theme-switcher/use-theme-transition.ts +62 -0
  373. package/src/components/timeline/index.ts +13 -0
  374. package/src/components/timeline/timeline.stories.tsx +99 -0
  375. package/src/components/timeline/timeline.test.tsx +196 -0
  376. package/src/components/timeline/timeline.tsx +188 -0
  377. package/src/components/toggle/index.ts +1 -0
  378. package/src/components/toggle/toggle.stories.tsx +116 -0
  379. package/src/components/toggle/toggle.test.tsx +119 -0
  380. package/src/components/toggle/toggle.tsx +64 -0
  381. package/src/components/toggle-group/index.ts +1 -0
  382. package/src/components/toggle-group/toggle-group.stories.tsx +110 -0
  383. package/src/components/toggle-group/toggle-group.tsx +59 -0
  384. package/src/components/toolbar/index.ts +14 -0
  385. package/src/components/toolbar/toolbar.stories.tsx +126 -0
  386. package/src/components/toolbar/toolbar.test.tsx +73 -0
  387. package/src/components/toolbar/toolbar.tsx +177 -0
  388. package/src/components/tooltip/index.ts +1 -0
  389. package/src/components/tooltip/tooltip.stories.tsx +67 -0
  390. package/src/components/tooltip/tooltip.tsx +27 -0
  391. package/src/components/top-nav/index.ts +1 -0
  392. package/src/components/top-nav/top-nav.stories.tsx +18 -0
  393. package/src/components/top-nav/top-nav.tsx +34 -0
  394. package/src/components/transfer/index.ts +2 -0
  395. package/src/components/transfer/transfer.stories.tsx +162 -0
  396. package/src/components/transfer/transfer.test.tsx +177 -0
  397. package/src/components/transfer/transfer.tsx +481 -0
  398. package/src/components/tree/index.ts +6 -0
  399. package/src/components/tree/tree.stories.tsx +618 -0
  400. package/src/components/tree/tree.test.tsx +699 -0
  401. package/src/components/tree/tree.tsx +1149 -0
  402. package/src/components/tree/use-tree-keyboard.ts +314 -0
  403. package/src/components/tree-select/index.ts +1 -0
  404. package/src/components/tree-select/tree-select.stories.tsx +127 -0
  405. package/src/components/tree-select/tree-select.test.tsx +40 -0
  406. package/src/components/tree-select/tree-select.tsx +137 -0
  407. package/src/components/typography/index.ts +31 -0
  408. package/src/components/typography/prose.test.tsx +48 -0
  409. package/src/components/typography/prose.tsx +194 -0
  410. package/src/components/typography/typography.stories.tsx +244 -0
  411. package/src/components/typography/typography.test.tsx +140 -0
  412. package/src/components/typography/typography.tsx +126 -0
  413. package/src/components/view-toolbar/index.ts +10 -0
  414. package/src/components/view-toolbar/view-toolbar.stories.tsx +342 -0
  415. package/src/components/view-toolbar/view-toolbar.test.tsx +262 -0
  416. package/src/components/view-toolbar/view-toolbar.tsx +320 -0
  417. package/src/components/virtual-select/index.ts +2 -0
  418. package/src/components/virtual-select/use-virtual-listbox.ts +132 -0
  419. package/src/components/virtual-select/virtual-select.stories.tsx +203 -0
  420. package/src/components/virtual-select/virtual-select.test.tsx +106 -0
  421. package/src/components/virtual-select/virtual-select.tsx +361 -0
  422. package/src/components/wizard/index.ts +12 -0
  423. package/src/components/wizard/wizard.stories.tsx +148 -0
  424. package/src/components/wizard/wizard.test.tsx +67 -0
  425. package/src/components/wizard/wizard.tsx +354 -0
  426. package/src/decoration.stories.tsx +236 -0
  427. package/src/index.ts +174 -0
  428. package/src/lib/cn.test.ts +39 -0
  429. package/src/lib/cn.ts +31 -0
  430. package/src/lib/compact-number.ts +61 -0
  431. package/src/lib/document-address.test.ts +75 -0
  432. package/src/lib/document-address.ts +265 -0
  433. package/src/lib/download.ts +93 -0
  434. package/src/lib/file-icon.test.ts +45 -0
  435. package/src/lib/file-icon.ts +58 -0
  436. package/src/lib/file-kind.test.ts +98 -0
  437. package/src/lib/file-kind.ts +303 -0
  438. package/src/lib/file-source.test.ts +217 -0
  439. package/src/lib/file-source.ts +269 -0
  440. package/src/lib/merge-refs.test.ts +37 -0
  441. package/src/lib/merge-refs.ts +17 -0
  442. package/src/lib/streamdown-translations.ts +99 -0
  443. package/src/lib/use-copy-to-clipboard.ts +75 -0
  444. package/src/lib/use-mobile.ts +16 -0
  445. package/src/motion.stories.tsx +266 -0
  446. package/src/taste-profile.test.tsx +252 -0
  447. package/src/templates-object-detail-hub.stories.tsx +466 -0
  448. package/src/templates-screen-states.stories.tsx +306 -0
  449. package/src/templates-settings.stories.tsx +245 -0
@@ -0,0 +1,165 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { SliderNumber } from "./slider-number";
5
+
6
+ describe("SliderNumber", () => {
7
+ it("renders a slider and a synced number input", () => {
8
+ render(<SliderNumber aria-label="Temperature" min={0} max={1} step={0.1} defaultValue={0.5} />);
9
+ expect(screen.getByRole("slider", { name: "Temperature" })).toHaveAttribute(
10
+ "aria-valuenow",
11
+ "0.5",
12
+ );
13
+ expect(screen.getByRole("spinbutton", { name: "Temperature" })).toHaveValue(0.5);
14
+ });
15
+
16
+ it("rounds slider-driven and typed values identically (lockstep)", async () => {
17
+ const onValueChange = vi.fn();
18
+ render(
19
+ <SliderNumber
20
+ aria-label="Temperature"
21
+ min={0}
22
+ max={1}
23
+ step={0.1}
24
+ defaultValue={0}
25
+ onValueChange={onValueChange}
26
+ />,
27
+ );
28
+ const thumb = screen.getByRole("slider", { name: "Temperature" });
29
+ thumb.focus();
30
+ await userEvent.keyboard("{ArrowRight}{ArrowRight}{ArrowRight}");
31
+ expect(onValueChange).toHaveBeenLastCalledWith(0.3);
32
+
33
+ const numberInput = screen.getByRole("spinbutton", { name: "Temperature" });
34
+ await userEvent.clear(numberInput);
35
+ await userEvent.type(numberInput, "0.3");
36
+ await userEvent.tab();
37
+ expect(onValueChange).toHaveBeenLastCalledWith(0.3);
38
+ });
39
+
40
+ it("null is a real state distinct from 0, reachable via reset", async () => {
41
+ const onValueChange = vi.fn();
42
+ render(
43
+ <SliderNumber
44
+ aria-label="Temperature"
45
+ min={0}
46
+ max={1}
47
+ step={0.1}
48
+ defaultValue={0.6}
49
+ onValueChange={onValueChange}
50
+ />,
51
+ );
52
+ await userEvent.click(screen.getByRole("button", { name: "Reset" }));
53
+ expect(onValueChange).toHaveBeenCalledWith(null);
54
+ });
55
+
56
+ it("disables the reset control once the value is already null", () => {
57
+ render(<SliderNumber aria-label="Temperature" defaultValue={null} />);
58
+ expect(screen.getByRole("button", { name: "Reset" })).toBeDisabled();
59
+ });
60
+
61
+ it("shows the slider thumb as dimmed/inactive while value is the provider default (null)", () => {
62
+ render(
63
+ <SliderNumber aria-label="Temperature" min={0} max={1} step={0.1} defaultValue={null} />,
64
+ );
65
+ const classes = screen.getByRole("slider", { name: "Temperature" }).className.split(/\s+/);
66
+ // Exact token match — `disabled:opacity-50` is always present as a base
67
+ // (disabled-state) utility, so a substring match would false-positive.
68
+ expect(classes).toContain("opacity-50");
69
+ expect(classes).toContain("border-dashed");
70
+ });
71
+
72
+ it("shows the slider thumb as active (not dimmed) once a real value is set", () => {
73
+ render(<SliderNumber aria-label="Temperature" min={0} max={1} step={0.1} defaultValue={0.4} />);
74
+ const classes = screen.getByRole("slider", { name: "Temperature" }).className.split(/\s+/);
75
+ expect(classes).not.toContain("opacity-50");
76
+ expect(classes).not.toContain("border-dashed");
77
+ });
78
+
79
+ it("round-trips null -> non-null -> reset -> null", async () => {
80
+ const onValueChange = vi.fn();
81
+ render(
82
+ <SliderNumber
83
+ aria-label="Temperature"
84
+ min={0}
85
+ max={1}
86
+ step={0.1}
87
+ defaultValue={null}
88
+ onValueChange={onValueChange}
89
+ />,
90
+ );
91
+ const thumb = screen.getByRole("slider", { name: "Temperature" });
92
+ thumb.focus();
93
+ await userEvent.keyboard("{ArrowRight}");
94
+ expect(onValueChange).toHaveBeenLastCalledWith(0.1);
95
+
96
+ const resetBtn = screen.getByRole("button", { name: "Reset" });
97
+ await userEvent.click(resetBtn);
98
+ expect(onValueChange).toHaveBeenLastCalledWith(null);
99
+ expect(resetBtn).toBeDisabled();
100
+ });
101
+
102
+ it("supports the controlled mode via value/onValueChange", async () => {
103
+ const onValueChange = vi.fn();
104
+ render(
105
+ <SliderNumber
106
+ aria-label="Temperature"
107
+ min={0}
108
+ max={1}
109
+ step={0.1}
110
+ value={0.2}
111
+ onValueChange={onValueChange}
112
+ />,
113
+ );
114
+ const numberInput = screen.getByRole("spinbutton", { name: "Temperature" });
115
+ await userEvent.clear(numberInput);
116
+ await userEvent.type(numberInput, "0.9");
117
+ await userEvent.tab();
118
+ expect(onValueChange).toHaveBeenLastCalledWith(0.9);
119
+ });
120
+
121
+ it.each([
122
+ ["rounds to the value already held", "0.44", "0.4"],
123
+ ["rounds to a different value", "0.66", "0.7"],
124
+ ["is clamped to max", "9", "1"],
125
+ ])(
126
+ "input display never disagrees with the slider when a typed value %s",
127
+ async (_case, typed, expected) => {
128
+ render(<SliderNumber aria-label="T" min={0} max={1} step={0.1} defaultValue={0.4} />);
129
+ await userEvent.clear(screen.getByRole("spinbutton", { name: "T" }));
130
+ await userEvent.type(screen.getByRole("spinbutton", { name: "T" }), typed);
131
+ await userEvent.tab();
132
+ // Re-query: correcting the display can remount the input, so a node
133
+ // captured before the blur would be a detached copy.
134
+ const shown = (screen.getByRole("spinbutton", { name: "T" }) as HTMLInputElement).value;
135
+ const announced = screen.getByRole("slider", { name: "T" }).getAttribute("aria-valuenow");
136
+ expect(announced).toBe(expected);
137
+ // The "rounds to the value already held" row is the one that regressed:
138
+ // `NumberInput` only re-derives its text when the controlled value
139
+ // CHANGES, so it kept showing "0.44" beside a slider reading 0.4.
140
+ expect(shown).toBe(expected);
141
+ },
142
+ );
143
+
144
+ it("keeps its default width shrinkable and caller-overridable", () => {
145
+ const { container, rerender } = render(<SliderNumber aria-label="Temperature" />);
146
+ const root = () => container.querySelector('[data-slot="slider-number"]')!;
147
+ // `w-80` is a PREFERRED width — `max-w-full` stops it overflowing a
148
+ // narrower container instead of forcing a horizontal scrollbar.
149
+ expect(root()).toHaveClass("w-80", "max-w-full");
150
+
151
+ rerender(<SliderNumber aria-label="Temperature" className="w-full" />);
152
+ expect(root()).toHaveClass("w-full");
153
+ expect(root()).not.toHaveClass("w-80");
154
+ });
155
+
156
+ it("is disabled when disabled prop is set", () => {
157
+ render(<SliderNumber aria-label="Temperature" defaultValue={0.5} disabled />);
158
+ expect(screen.getByRole("slider", { name: "Temperature" })).toHaveAttribute(
159
+ "data-disabled",
160
+ "",
161
+ );
162
+ expect(screen.getByRole("spinbutton", { name: "Temperature" })).toBeDisabled();
163
+ expect(screen.getByRole("button", { name: "Reset" })).toBeDisabled();
164
+ });
165
+ });
@@ -0,0 +1,191 @@
1
+ "use client";
2
+
3
+ import { forwardRef, useCallback, useState, type HTMLAttributes, type ReactNode } from "react";
4
+ import { cn } from "../../lib/cn";
5
+ import { Button } from "../button";
6
+ import { NumberInput } from "../number-input";
7
+ import { Slider } from "../slider";
8
+ import { useLocale } from "../locale-provider";
9
+
10
+ export interface SliderNumberProps extends Omit<
11
+ HTMLAttributes<HTMLDivElement>,
12
+ "onChange" | "defaultValue"
13
+ > {
14
+ /** Controlled value. Pass `null` for "use the provider default". */
15
+ value?: number | null;
16
+ /** Uncontrolled initial value. */
17
+ defaultValue?: number | null;
18
+ /** Called when the value changes — always rounded identically on the drag and type paths. */
19
+ onValueChange?: (value: number | null) => void;
20
+ /** Minimum allowed value. Defaults to 0. */
21
+ min?: number;
22
+ /** Maximum allowed value. Defaults to 100. */
23
+ max?: number;
24
+ /** Step increment for both the slider and the number input's arrow keys. Defaults to 1. */
25
+ step?: number;
26
+ /**
27
+ * Number of decimal places to round to. Defaults to the decimal places in
28
+ * `step` (e.g. `step={0.1}` rounds to 1 decimal). Applied identically on
29
+ * BOTH the slider path and the number-input path so drag-then-type never
30
+ * disagrees.
31
+ */
32
+ precision?: number;
33
+ /** Label for the reset-to-provider-default control. Defaults to "Reset". */
34
+ resetLabel?: ReactNode;
35
+ /** Disables the whole control. */
36
+ disabled?: boolean;
37
+ id?: string;
38
+ name?: string;
39
+ "aria-label"?: string;
40
+ "aria-labelledby"?: string;
41
+ }
42
+
43
+ function getStepDecimals(step: number): number {
44
+ const str = step.toString();
45
+ const dot = str.indexOf(".");
46
+ return dot === -1 ? 0 : str.length - dot - 1;
47
+ }
48
+
49
+ function clampValue(value: number, min?: number, max?: number): number {
50
+ let result = value;
51
+ if (min !== undefined) result = Math.max(min, result);
52
+ if (max !== undefined) result = Math.min(max, result);
53
+ return result;
54
+ }
55
+
56
+ /** Rounds to a fixed decimal precision — the ONE function both paths call. */
57
+ function roundValue(value: number, step: number, precision?: number): number {
58
+ const decimals = precision ?? getStepDecimals(step);
59
+ const factor = 10 ** decimals;
60
+ return Math.round(value * factor) / factor;
61
+ }
62
+
63
+ /**
64
+ * SliderNumber — a `Slider` and a `NumberInput` bound to one value, clamped
65
+ * and rounded in lockstep so they can never disagree, plus an explicit
66
+ * `null` state meaning "use the provider default" with a reset control.
67
+ *
68
+ * The subtle part (#372): both the slider's `onValueChange` and the number
69
+ * input's `onValueChange` are routed through the SAME `commit` function,
70
+ * which rounds to a fixed decimal precision before storing — so dragging to
71
+ * a value and then typing the same value never produces two representations
72
+ * of the same intent (e.g. floating-point drift like `0.30000000000000004`).
73
+ */
74
+ export const SliderNumber = forwardRef<HTMLDivElement, SliderNumberProps>(function SliderNumber(
75
+ {
76
+ value: valueProp,
77
+ defaultValue,
78
+ onValueChange,
79
+ min = 0,
80
+ max = 100,
81
+ step = 1,
82
+ precision,
83
+ resetLabel,
84
+ disabled,
85
+ id,
86
+ name,
87
+ "aria-label": ariaLabel,
88
+ "aria-labelledby": ariaLabelledby,
89
+ className,
90
+ ...props
91
+ },
92
+ ref,
93
+ ) {
94
+ const { t } = useLocale();
95
+ const isControlled = valueProp !== undefined;
96
+ const [internalValue, setInternalValue] = useState<number | null>(
97
+ defaultValue !== undefined ? defaultValue : null,
98
+ );
99
+ const currentValue = isControlled ? valueProp : internalValue;
100
+
101
+ // Bumped whenever this component normalises a typed value back to the value
102
+ // it ALREADY holds. `NumberInput` only re-derives its display text when the
103
+ // controlled `value` prop CHANGES, so in that one case it would keep showing
104
+ // the raw keystrokes ("0.44") while the slider and the committed value read
105
+ // the rounded one (0.4) — the exact "slider and input disagree" failure this
106
+ // component exists to prevent. Remounting it re-derives the text from the
107
+ // value that was actually accepted. It only ever fires on blur, when the
108
+ // input is no longer focused, so the remount is invisible.
109
+ const [inputEpoch, setInputEpoch] = useState(0);
110
+
111
+ const commit = useCallback(
112
+ (next: number | null) => {
113
+ const rounded = next == null ? null : roundValue(clampValue(next, min, max), step, precision);
114
+ if (next != null && rounded !== next && rounded === currentValue) {
115
+ setInputEpoch((n) => n + 1);
116
+ }
117
+ if (!isControlled) setInternalValue(rounded);
118
+ onValueChange?.(rounded);
119
+ },
120
+ [isControlled, onValueChange, min, max, step, precision, currentValue],
121
+ );
122
+
123
+ const isDefault = currentValue == null;
124
+ const sliderValue = currentValue ?? min;
125
+
126
+ return (
127
+ <div
128
+ ref={ref}
129
+ data-slot="slider-number"
130
+ // A default FIXED width (matching Slider's own convention — it has no
131
+ // intrinsic width either, and its stories always pass one via
132
+ // className) so the Slider's `flex-1` has real space to grow into.
133
+ // `w-full`/percentage widths are unresolvable inside an ancestor with
134
+ // no definite width (e.g. Storybook's default "centered" layout, or a
135
+ // bare inline/flex context), which collapses `flex-1` to near-zero.
136
+ // Callers in a definite-width layout can still override via `className`
137
+ // (e.g. `className="w-full"` inside a form field's own fixed-width row).
138
+ // `max-w-full` keeps the fixed width from OVERFLOWING a container
139
+ // narrower than 20rem (a settings drawer, a 320px mobile column) — it is
140
+ // a preferred width, not a floor.
141
+ className={cn("flex w-80 max-w-full items-center gap-3", className)}
142
+ {...props}
143
+ >
144
+ <Slider
145
+ data-slot="slider-number-slider"
146
+ aria-label={ariaLabel}
147
+ aria-labelledby={ariaLabelledby}
148
+ min={min}
149
+ max={max}
150
+ step={step}
151
+ disabled={disabled}
152
+ value={[sliderValue]}
153
+ onValueChange={(values) => commit(values[0] ?? null)}
154
+ thumbProps={{
155
+ // Visually distinct "provider default" thumb — dimmed + dashed,
156
+ // never confusable with a real value (including 0).
157
+ className: cn(isDefault && "opacity-50 border-dashed"),
158
+ }}
159
+ className="flex-1"
160
+ />
161
+ <NumberInput
162
+ key={inputEpoch}
163
+ data-slot="slider-number-input"
164
+ id={id}
165
+ name={name}
166
+ aria-label={ariaLabel}
167
+ aria-labelledby={ariaLabelledby}
168
+ min={min}
169
+ max={max}
170
+ step={step}
171
+ disabled={disabled}
172
+ value={currentValue}
173
+ onValueChange={commit}
174
+ // NumberInput's own default is w-36 (144px); shrinking it below
175
+ // ~w-32 leaves too little room for the digits once its own
176
+ // decrease/increase buttons are subtracted, clipping the text.
177
+ className="w-32 shrink-0"
178
+ />
179
+ <Button
180
+ type="button"
181
+ variant="ghost"
182
+ size="sm"
183
+ data-slot="slider-number-reset"
184
+ disabled={disabled || isDefault}
185
+ onClick={() => commit(null)}
186
+ >
187
+ {resetLabel ?? t("ui.sliderNumber.reset")}
188
+ </Button>
189
+ </div>
190
+ );
191
+ });
@@ -0,0 +1 @@
1
+ export { Toaster, toast, type ToasterProps } from "./sonner";
@@ -0,0 +1,124 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, waitFor, within } from "storybook/test";
3
+ import { Button } from "../button";
4
+ import { Toaster, toast } from "./sonner";
5
+ const meta = {
6
+ title: "Feedback/Toast",
7
+ component: Toaster,
8
+ tags: ["autodocs"],
9
+ argTypes: {
10
+ position: {
11
+ description: "Where the toast stack appears on screen.",
12
+ control: { type: "select" },
13
+ options: [
14
+ "top-left",
15
+ "top-center",
16
+ "top-right",
17
+ "bottom-left",
18
+ "bottom-center",
19
+ "bottom-right",
20
+ ],
21
+ table: { category: "Layout" },
22
+ },
23
+ expand: {
24
+ description: "Expand all toasts in the stack by default.",
25
+ control: "boolean",
26
+ table: { category: "Behaviour" },
27
+ },
28
+ richColors: {
29
+ description: "Use rich semantic colours for success/error/warning toasts.",
30
+ control: "boolean",
31
+ table: { category: "Appearance" },
32
+ },
33
+ closeButton: {
34
+ description: "Show an explicit close button on each toast.",
35
+ control: "boolean",
36
+ table: { category: "Behaviour" },
37
+ },
38
+ duration: {
39
+ description: "Auto-dismiss duration in milliseconds. Default 4000.",
40
+ control: "number",
41
+ table: { category: "Behaviour" },
42
+ },
43
+ visibleToasts: {
44
+ description: "Maximum number of toasts visible at one time.",
45
+ control: "number",
46
+ table: { category: "Behaviour" },
47
+ },
48
+ className: {
49
+ description: "Additional CSS classes applied to the Toaster root.",
50
+ control: "text",
51
+ table: { category: "Styling" },
52
+ },
53
+ toastOptions: {
54
+ description: "Default options applied to every toast (classNames, style, etc.).",
55
+ control: false,
56
+ table: { category: "Behaviour" },
57
+ },
58
+ },
59
+ } satisfies Meta<typeof Toaster>;
60
+ export default meta;
61
+ type Story = StoryObj<typeof meta>;
62
+ export const Default: Story = {
63
+ render: () => (
64
+ <div>
65
+ <Button onClick={() => toast("Deployment started", { description: "billing@v1.4.2" })}>
66
+ Show toast
67
+ </Button>
68
+ <Toaster duration={4000} />
69
+ </div>
70
+ ),
71
+ // Clicks the trigger button, then confirms the toast message appears in the
72
+ // document body (Sonner renders outside the story canvas in a portal).
73
+ play: async ({ canvas, canvasElement, userEvent }) => {
74
+ await userEvent.click(canvas.getByRole("button", { name: /show toast/i }));
75
+ const body = within(canvasElement.ownerDocument.body);
76
+ const toastEl = await body.findByText("Deployment started");
77
+ await waitFor(() => expect(toastEl).toBeVisible());
78
+ },
79
+ };
80
+
81
+ export const CustomizedWithClassName: Story = {
82
+ render: () => (
83
+ <div>
84
+ <Button
85
+ onClick={() => toast("Hello", { description: "Custom className is merged with defaults" })}
86
+ >
87
+ Show toast
88
+ </Button>
89
+ <Toaster className="custom-wrapper-class" duration={4000} />
90
+ </div>
91
+ ),
92
+ play: async ({ canvas, canvasElement, userEvent }) => {
93
+ await userEvent.click(canvas.getByRole("button", { name: /show toast/i }));
94
+ const body = within(canvasElement.ownerDocument.body);
95
+ const toastEl = await body.findByText("Hello");
96
+ await waitFor(() => expect(toastEl).toBeVisible());
97
+ },
98
+ };
99
+
100
+ export const CustomizedWithToastOptions: Story = {
101
+ render: () => (
102
+ <div>
103
+ <Button
104
+ onClick={() => toast("Success!", { description: "Custom toast styles via toastOptions" })}
105
+ >
106
+ Show toast
107
+ </Button>
108
+ <Toaster
109
+ toastOptions={{
110
+ classNames: {
111
+ toast: "group-[.toaster]:bg-primary group-[.toaster]:text-primary-foreground",
112
+ },
113
+ }}
114
+ duration={4000}
115
+ />
116
+ </div>
117
+ ),
118
+ play: async ({ canvas, canvasElement, userEvent }) => {
119
+ await userEvent.click(canvas.getByRole("button", { name: /show toast/i }));
120
+ const body = within(canvasElement.ownerDocument.body);
121
+ const toastEl = await body.findByText("Success!");
122
+ await waitFor(() => expect(toastEl).toBeVisible());
123
+ },
124
+ };
@@ -0,0 +1,76 @@
1
+ import { beforeAll, describe, expect, it } from "vitest";
2
+ import { render, waitFor } from "@testing-library/react";
3
+ import { Toaster, toast } from "./sonner";
4
+
5
+ // jsdom does not provide ResizeObserver, which sonner uses internally to
6
+ // measure each toast's height.
7
+ beforeAll(() => {
8
+ if (typeof window !== "undefined" && !("ResizeObserver" in window)) {
9
+ // @ts-expect-error jsdom stub
10
+ window.ResizeObserver = class ResizeObserver {
11
+ observe() {}
12
+ unobserve() {}
13
+ disconnect() {}
14
+ };
15
+ }
16
+ });
17
+
18
+ describe("Toaster", () => {
19
+ it('merges a consumer\'s className with the default "toaster group" wrapper classes (#389)', async () => {
20
+ render(<Toaster className="my-custom-class" />);
21
+ toast("Hello world");
22
+
23
+ // Wait for the wrapper (ol element) to appear
24
+ const wrapper = await waitFor(() => {
25
+ const el = document.querySelector("[data-sonner-toaster]");
26
+ expect(el).not.toBeNull();
27
+ return el as HTMLElement;
28
+ });
29
+
30
+ // The wrapper must contain BOTH the default classes AND the consumer's class
31
+ expect(wrapper).toHaveClass("toaster");
32
+ expect(wrapper).toHaveClass("group");
33
+ expect(wrapper).toHaveClass("my-custom-class");
34
+ });
35
+
36
+ it("merges a consumer's toastOptions.classNames into the defaults instead of replacing them (#362)", async () => {
37
+ render(<Toaster toastOptions={{ classNames: { toast: "extra-class" } }} />);
38
+ toast("Deployment started", {
39
+ description: "billing@v1.4.2",
40
+ action: { label: "Undo", onClick: () => {} },
41
+ cancel: { label: "Dismiss", onClick: () => {} },
42
+ });
43
+
44
+ const toastEl = await waitFor(() => {
45
+ const el = document.querySelector("[data-sonner-toast]");
46
+ expect(el).not.toBeNull();
47
+ return el as HTMLElement;
48
+ });
49
+
50
+ // The consumer's override is present...
51
+ expect(toastEl.className).toContain("extra-class");
52
+ // ...EXTENDING the default `toast` classes, not replacing them.
53
+ expect(toastEl.className).toContain("group-[.toaster]:bg-card");
54
+
55
+ // Sibling classNames the consumer did NOT touch still carry their
56
+ // library-default token-backed classes — proving the merge is deep
57
+ // (per-key), not just that `toast` itself survived.
58
+ expect(document.querySelector("[data-description]")).toHaveClass(
59
+ "group-[.toast]:text-muted-foreground",
60
+ );
61
+ expect(document.querySelector("[data-action]")).toHaveClass("group-[.toast]:bg-primary");
62
+ expect(document.querySelector("[data-cancel]")).toHaveClass("group-[.toast]:bg-muted");
63
+ });
64
+
65
+ it("<Toaster /> with no toastOptions renders the default classes unchanged (regression guard)", async () => {
66
+ render(<Toaster />);
67
+ toast("Plain toast");
68
+
69
+ const toastEl = await waitFor(() => {
70
+ const el = document.querySelector("[data-sonner-toast]");
71
+ expect(el).not.toBeNull();
72
+ return el as HTMLElement;
73
+ });
74
+ expect(toastEl).toHaveClass("group-[.toaster]:bg-card");
75
+ });
76
+ });
@@ -0,0 +1,101 @@
1
+ import { useEffect, useState } from "react";
2
+ import { Toaster as SonnerToaster, toast } from "sonner";
3
+ import { resolveThemeIsDark } from "@elabs-ai/components-tokens";
4
+ import { cn } from "../../lib/cn";
5
+
6
+ /**
7
+ * The library's default token-backed toast class names. Kept as a standalone
8
+ * constant (rather than inline in the JSX) so `Toaster` can merge a
9
+ * consumer's `toastOptions.classNames` INTO these defaults instead of
10
+ * shallow-replacing them (#362) — each key is combined via `cn()` so a
11
+ * consumer's override extends the default class string rather than
12
+ * discarding it.
13
+ */
14
+ const DEFAULT_TOAST_CLASS_NAMES = {
15
+ toast:
16
+ "group toast group-[.toaster]:bg-card group-[.toaster]:text-card-foreground group-[.toaster]:shadow-ring-lg",
17
+ description: "group-[.toast]:text-muted-foreground",
18
+ actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
19
+ cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
20
+ } as const;
21
+
22
+ /**
23
+ * Reads the active theme's light/dark-ness off the document. Uses
24
+ * `resolveThemeIsDark` — the theme's own `color-scheme` — rather than a registry
25
+ * lookup, so a consumer-authored dark theme gets dark toasts with no
26
+ * registration (ADR 0029).
27
+ */
28
+ function useDocumentThemeMode(): "light" | "dark" {
29
+ const [mode, setMode] = useState<"light" | "dark">("light");
30
+ useEffect(() => {
31
+ if (typeof document === "undefined") return;
32
+ const el = document.documentElement;
33
+ const read = () => setMode(resolveThemeIsDark(el) ? "dark" : "light");
34
+ read();
35
+ const obs = new MutationObserver(read);
36
+ obs.observe(el, { attributes: true, attributeFilter: ["data-theme"] });
37
+ return () => obs.disconnect();
38
+ }, []);
39
+ return mode;
40
+ }
41
+
42
+ export type ToasterProps = React.ComponentProps<typeof SonnerToaster>;
43
+
44
+ /**
45
+ * Toast host. Mount once near the app root. Trigger toasts with the exported
46
+ * `toast()`. Theme follows the active brand theme (via `data-theme`), so it
47
+ * works with or without <ThemeProvider>.
48
+ *
49
+ * ## Customizing toast styling
50
+ *
51
+ * The Toaster wrapper renders with the classes `toaster group` by default.
52
+ * These classes are **merged** with any consumer-supplied `className`, not
53
+ * replaced, so that built-in styling (the `.group-[.toaster]:*` selectors
54
+ * on individual toasts) continues to work.
55
+ *
56
+ * To override a built-in toast class, **mirror the variant prefix** when you
57
+ * pass `toastOptions.classNames`. Built-in toast classes are variant-scoped
58
+ * (e.g. `group-[.toaster]:bg-card`), so a bare utility override will be
59
+ * outspecificity'd by the built-in and appear to do nothing. Instead, use:
60
+ *
61
+ * ```tsx
62
+ * <Toaster
63
+ * toastOptions={{
64
+ * classNames: {
65
+ * toast: "group-[.toaster]:bg-primary", // ✓ mirrors the prefix
66
+ * }
67
+ * }}
68
+ * />
69
+ * ```
70
+ *
71
+ * ## Theme handling
72
+ *
73
+ * The Toaster's `theme` prop is automatically set from the active theme's own
74
+ * `color-scheme` declaration, so toasts visually track the app's theme by
75
+ * default — including a theme you authored yourself. However, you may pass
76
+ * `theme="light"` or `theme="dark"` directly to override this derivation if needed.
77
+ */
78
+ export function Toaster({ className, toastOptions, ...rest }: ToasterProps) {
79
+ const mode = useDocumentThemeMode();
80
+ const userClassNames = toastOptions?.classNames;
81
+ // Merge (not replace): every default key is extended with the consumer's
82
+ // override via cn(); any extra keys the consumer passes (e.g. `success`,
83
+ // `closeButton`) pass through untouched.
84
+ const classNames = {
85
+ ...userClassNames,
86
+ toast: cn(DEFAULT_TOAST_CLASS_NAMES.toast, userClassNames?.toast),
87
+ description: cn(DEFAULT_TOAST_CLASS_NAMES.description, userClassNames?.description),
88
+ actionButton: cn(DEFAULT_TOAST_CLASS_NAMES.actionButton, userClassNames?.actionButton),
89
+ cancelButton: cn(DEFAULT_TOAST_CLASS_NAMES.cancelButton, userClassNames?.cancelButton),
90
+ };
91
+ return (
92
+ <SonnerToaster
93
+ theme={mode}
94
+ className={cn("toaster group", className)}
95
+ toastOptions={{ ...toastOptions, classNames }}
96
+ {...rest}
97
+ />
98
+ );
99
+ }
100
+
101
+ export { toast };
@@ -0,0 +1 @@
1
+ export { Spinner, type SpinnerProps } from "./spinner";