@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,28 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Skeleton } from "./skeleton";
3
+
4
+ const meta = {
5
+ title: "States/Skeleton",
6
+ component: Skeleton,
7
+ tags: ["autodocs"],
8
+ argTypes: {
9
+ className: {
10
+ description:
11
+ "Width/height and shape utilities applied to the skeleton (e.g. `h-4 w-32 rounded-full`).",
12
+ control: "text",
13
+ table: { category: "Styling" },
14
+ },
15
+ },
16
+ } satisfies Meta<typeof Skeleton>;
17
+ export default meta;
18
+ type Story = StoryObj<typeof meta>;
19
+
20
+ export const Card: Story = {
21
+ render: () => (
22
+ <div className="w-64 space-y-3">
23
+ <Skeleton className="h-32 w-full" />
24
+ <Skeleton className="h-4 w-3/4" />
25
+ <Skeleton className="h-4 w-1/2" />
26
+ </div>
27
+ ),
28
+ };
@@ -0,0 +1,15 @@
1
+ import { type HTMLAttributes } from "react";
2
+ import { cn } from "../../lib/cn";
3
+
4
+ export type SkeletonProps = HTMLAttributes<HTMLDivElement>;
5
+
6
+ /** Loading placeholder. Compose with width/height utilities, e.g. `h-4 w-32`. */
7
+ export function Skeleton({ className, ...props }: SkeletonProps) {
8
+ return (
9
+ <div
10
+ className={cn("animate-pulse rounded-md bg-muted", className)}
11
+ aria-hidden="true"
12
+ {...props}
13
+ />
14
+ );
15
+ }
@@ -0,0 +1 @@
1
+ export { Slider, type SliderProps, type SliderThumbProps } from "./slider";
@@ -0,0 +1,211 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect } from "storybook/test";
3
+ import { Slider } from "./slider";
4
+ const meta = {
5
+ title: "Forms/Slider",
6
+ component: Slider,
7
+ tags: ["autodocs"],
8
+ argTypes: {
9
+ defaultValue: {
10
+ description: "Uncontrolled initial value(s) as an array.",
11
+ control: false,
12
+ table: { category: "State" },
13
+ },
14
+ value: {
15
+ description: "Controlled value(s) as an array.",
16
+ control: false,
17
+ table: { category: "State" },
18
+ },
19
+ min: {
20
+ description: "Minimum value of the range.",
21
+ control: "number",
22
+ table: { category: "Behavior" },
23
+ },
24
+ max: {
25
+ description: "Maximum value of the range.",
26
+ control: "number",
27
+ table: { category: "Behavior" },
28
+ },
29
+ step: {
30
+ description: "Increment between selectable values.",
31
+ control: "number",
32
+ table: { category: "Behavior" },
33
+ },
34
+ disabled: {
35
+ description: "Disables the slider.",
36
+ control: "boolean",
37
+ table: { category: "State" },
38
+ },
39
+ orientation: {
40
+ description: "Layout direction of the slider.",
41
+ control: { type: "radio" },
42
+ options: ["horizontal", "vertical"],
43
+ table: { category: "Appearance" },
44
+ },
45
+ onValueChange: {
46
+ description: "Called when the slider value changes.",
47
+ control: false,
48
+ table: { category: "Behavior" },
49
+ },
50
+ className: {
51
+ description: "Additional CSS classes applied to the root element.",
52
+ control: "text",
53
+ table: { category: "Appearance" },
54
+ },
55
+ },
56
+ } satisfies Meta<typeof Slider>;
57
+ export default meta;
58
+ type Story = StoryObj<typeof meta>;
59
+ export const Default: Story = {
60
+ render: () => <Slider defaultValue={[50]} max={100} step={1} className="w-64" />,
61
+ // Confirms the slider thumb is present and has the expected value attribute.
62
+ play: async ({ canvas }) => {
63
+ const thumb = canvas.getByRole("slider");
64
+ await expect(thumb).toBeInTheDocument();
65
+ await expect(thumb).toHaveAttribute("aria-valuenow", "50");
66
+ },
67
+ };
68
+
69
+ /**
70
+ * The name has to land on the THUMB — that is the element with `role="slider"`.
71
+ * `aria-label` (or `aria-labelledby`, for a visible `<Label>`) is routed there.
72
+ */
73
+ export const Labelled: Story = {
74
+ render: () => (
75
+ <Slider aria-label="Decoration level" defaultValue={[4]} max={10} step={1} className="w-64" />
76
+ ),
77
+ play: async ({ canvas }) => {
78
+ await expect(canvas.getByRole("slider", { name: "Decoration level" })).toBeInTheDocument();
79
+ },
80
+ };
81
+
82
+ /**
83
+ * A replay scrubber announces "step 3 of 12" instead of the bare numeric
84
+ * value — `aria-valuetext` on `thumbProps` (or the top-level `aria-valuetext`
85
+ * shorthand) lands on the thumb, the element that actually carries
86
+ * `role="slider"`.
87
+ */
88
+ export const CustomValueText: Story = {
89
+ render: () => (
90
+ <Slider
91
+ aria-label="Replay position"
92
+ defaultValue={[3]}
93
+ max={12}
94
+ step={1}
95
+ className="w-64"
96
+ thumbProps={{ "aria-valuetext": "step 3 of 12" }}
97
+ />
98
+ ),
99
+ play: async ({ canvas }) => {
100
+ const thumb = canvas.getByRole("slider", { name: "Replay position" });
101
+ await expect(thumb).toHaveAttribute("aria-valuetext", "step 3 of 12");
102
+ },
103
+ };
104
+
105
+ /**
106
+ * A range slider (array `defaultValue`) renders one thumb per value. Passing
107
+ * an ARRAY to `thumbProps` (#398) gives each thumb its own accessible name
108
+ * and `aria-valuetext` — a top-level `aria-label`/`aria-valuetext` would
109
+ * apply identically to both thumbs, which isn't what a min/max range wants.
110
+ */
111
+ export const Range: Story = {
112
+ render: () => (
113
+ <Slider
114
+ defaultValue={[20, 80]}
115
+ max={100}
116
+ step={1}
117
+ className="w-64"
118
+ thumbProps={[
119
+ { "aria-label": "Minimum price", "aria-valuetext": "$20" },
120
+ { "aria-label": "Maximum price", "aria-valuetext": "$80" },
121
+ ]}
122
+ />
123
+ ),
124
+ play: async ({ canvas }) => {
125
+ const thumbs = canvas.getAllByRole("slider");
126
+ await expect(thumbs).toHaveLength(2);
127
+ await expect(thumbs[0]).toHaveAccessibleName("Minimum price");
128
+ await expect(thumbs[0]).toHaveAttribute("aria-valuetext", "$20");
129
+ await expect(thumbs[1]).toHaveAccessibleName("Maximum price");
130
+ await expect(thumbs[1]).toHaveAttribute("aria-valuetext", "$80");
131
+
132
+ // #398 AC-3 — a real accessibility-tree assertion, not just a DOM
133
+ // attribute snapshot: the DOM checks above prove the markup is right,
134
+ // not that a screen reader actually hears two distinctly-named,
135
+ // distinctly-valued thumbs. This reads Chromium's live AX tree via CDP
136
+ // (`Accessibility.getFullAXTree`), which is what assistive tech consumes.
137
+ //
138
+ // `@vitest/browser/context` is a virtual module that only resolves inside
139
+ // Vitest's browser-mode test runner (`pnpm --filter @elabs-ai/components-docs test-storybook` /
140
+ // `vitest --project storybook run`, the same engine CI's blocking
141
+ // "Storybook interaction + axe" job uses) — importing it anywhere else
142
+ // throws by the module's own design. The dynamic import + catch lets this
143
+ // block run there while leaving plain interactive Storybook browsing and
144
+ // a production `build-storybook` unaffected (they still get every DOM
145
+ // assertion above).
146
+ let browserContext: typeof import("@vitest/browser/context") | undefined;
147
+ try {
148
+ browserContext = await import("@vitest/browser/context");
149
+ } catch {
150
+ browserContext = undefined;
151
+ }
152
+ if (!browserContext) return;
153
+
154
+ interface AXProperty {
155
+ name: string;
156
+ value?: { value?: unknown };
157
+ }
158
+ interface AXNode {
159
+ role?: { value?: string };
160
+ name?: { value?: string };
161
+ ignored?: boolean;
162
+ properties?: AXProperty[];
163
+ }
164
+ interface FrameNode {
165
+ frame: { id: string };
166
+ childFrames?: FrameNode[];
167
+ }
168
+ // The base `CDPSession` type ships empty ("methods are defined by the
169
+ // provider type augmentation" — @vitest/browser/context.d.ts) and this
170
+ // file doesn't pull in the playwright provider's ambient augmentation, so
171
+ // `send` is typed by hand here rather than importing playwright's CDP
172
+ // protocol types into a story file.
173
+ interface CdpSession {
174
+ send: (method: string, params?: Record<string, unknown>) => Promise<unknown>;
175
+ }
176
+
177
+ const session = browserContext.cdp() as unknown as CdpSession;
178
+ await session.send("Accessibility.enable");
179
+ await session.send("Page.enable");
180
+ const { frameTree } = (await session.send("Page.getFrameTree")) as {
181
+ frameTree: FrameNode;
182
+ };
183
+ const frames: { id: string }[] = [];
184
+ const collectFrames = (node: FrameNode) => {
185
+ frames.push(node.frame);
186
+ for (const child of node.childFrames ?? []) collectFrames(child);
187
+ };
188
+ collectFrames(frameTree);
189
+
190
+ let axNodes: AXNode[] = [];
191
+ for (const frame of frames) {
192
+ const result = (await session.send("Accessibility.getFullAXTree", {
193
+ frameId: frame.id,
194
+ })) as { nodes: AXNode[] };
195
+ axNodes = axNodes.concat(result.nodes);
196
+ }
197
+
198
+ const sliderNodes = axNodes.filter((node) => node.role?.value === "slider" && !node.ignored);
199
+ await expect(sliderNodes).toHaveLength(2);
200
+
201
+ const valuetextOf = (node: AXNode) =>
202
+ node.properties?.find((property) => property.name === "valuetext")?.value?.value;
203
+ const namedThumbs = sliderNodes
204
+ .map((node) => ({ name: node.name?.value, valuetext: valuetextOf(node) }))
205
+ .sort((a, b) => String(a.name).localeCompare(String(b.name)));
206
+ await expect(namedThumbs).toEqual([
207
+ { name: "Maximum price", valuetext: "$80" },
208
+ { name: "Minimum price", valuetext: "$20" },
209
+ ]);
210
+ },
211
+ };
@@ -0,0 +1,168 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { Slider, type SliderThumbProps } from "./slider";
4
+
5
+ describe("Slider", () => {
6
+ it("forwards aria-label to the thumb (role=slider)", () => {
7
+ render(<Slider aria-label="Volume" defaultValue={[50]} max={100} step={1} />);
8
+ expect(screen.getByRole("slider", { name: "Volume" })).toBeInTheDocument();
9
+ });
10
+
11
+ it("forwards the top-level aria-valuetext to the thumb", () => {
12
+ render(
13
+ <Slider
14
+ aria-label="Replay position"
15
+ aria-valuetext="step 3 of 12"
16
+ defaultValue={[3]}
17
+ max={12}
18
+ step={1}
19
+ />,
20
+ );
21
+ expect(screen.getByRole("slider")).toHaveAttribute("aria-valuetext", "step 3 of 12");
22
+ });
23
+
24
+ it("forwards thumbProps.aria-valuetext to the thumb", () => {
25
+ render(
26
+ <Slider
27
+ aria-label="Replay position"
28
+ defaultValue={[3]}
29
+ max={12}
30
+ step={1}
31
+ thumbProps={{ "aria-valuetext": "step 3 of 12" }}
32
+ />,
33
+ );
34
+ expect(screen.getByRole("slider")).toHaveAttribute("aria-valuetext", "step 3 of 12");
35
+ });
36
+
37
+ it("thumbProps wins over the top-level aria-label when both are set", () => {
38
+ render(
39
+ <Slider
40
+ aria-label="Root label"
41
+ defaultValue={[3]}
42
+ max={12}
43
+ step={1}
44
+ thumbProps={{ "aria-label": "Thumb label" }}
45
+ />,
46
+ );
47
+ expect(screen.getByRole("slider", { name: "Thumb label" })).toBeInTheDocument();
48
+ });
49
+
50
+ it("does not regress existing aria-labelledby forwarding", () => {
51
+ render(
52
+ <>
53
+ <label id="vol-label">Volume</label>
54
+ <Slider aria-labelledby="vol-label" defaultValue={[50]} max={100} step={1} />
55
+ </>,
56
+ );
57
+ expect(screen.getByRole("slider", { name: "Volume" })).toBeInTheDocument();
58
+ });
59
+
60
+ // Radix spreads consumer thumb props AFTER its own role/value/tabIndex, so
61
+ // an unfiltered escape hatch lets a consumer destroy the widget.
62
+ it("ignores thumbProps keys that would break the slider widget", () => {
63
+ render(
64
+ <Slider
65
+ aria-label="Volume"
66
+ defaultValue={[50]}
67
+ max={100}
68
+ step={1}
69
+ thumbProps={{
70
+ role: "presentation",
71
+ "aria-valuenow": 999,
72
+ "aria-valuemin": 999,
73
+ "aria-valuemax": 999,
74
+ "aria-orientation": "vertical",
75
+ tabIndex: -1,
76
+ id: "volume-thumb",
77
+ }}
78
+ />,
79
+ );
80
+ const thumb = screen.getByRole("slider", { name: "Volume" });
81
+ expect(thumb).toHaveAttribute("aria-valuenow", "50");
82
+ expect(thumb).toHaveAttribute("aria-valuemin", "0");
83
+ expect(thumb).toHaveAttribute("aria-valuemax", "100");
84
+ expect(thumb).toHaveAttribute("aria-orientation", "horizontal");
85
+ expect(thumb).toHaveAttribute("tabindex", "0");
86
+ // Non-protected keys still pass through.
87
+ expect(thumb).toHaveAttribute("id", "volume-thumb");
88
+ });
89
+
90
+ // #398 — a range slider (array value) renders one thumb per value, and a
91
+ // matching array of thumbProps gives each one its own accessible name.
92
+ describe("range (multi-thumb) support", () => {
93
+ it("renders one thumb per value in a range slider", () => {
94
+ render(<Slider aria-label="Price range" defaultValue={[3, 9]} max={12} step={1} />);
95
+ expect(screen.getAllByRole("slider")).toHaveLength(2);
96
+ });
97
+
98
+ it("gives each thumb its own aria-valuetext via an array of thumbProps", () => {
99
+ render(
100
+ <Slider
101
+ defaultValue={[3, 9]}
102
+ max={12}
103
+ step={1}
104
+ thumbProps={[
105
+ { "aria-label": "Minimum price", "aria-valuetext": "$3" },
106
+ { "aria-label": "Maximum price", "aria-valuetext": "$9" },
107
+ ]}
108
+ />,
109
+ );
110
+ const [low, high] = screen.getAllByRole("slider");
111
+ expect(low).toHaveAttribute("aria-valuetext", "$3");
112
+ expect(low).toHaveAttribute("aria-label", "Minimum price");
113
+ expect(high).toHaveAttribute("aria-valuetext", "$9");
114
+ expect(high).toHaveAttribute("aria-label", "Maximum price");
115
+ });
116
+
117
+ it("gives each thumb its own data-* attributes via an array of thumbProps", () => {
118
+ render(
119
+ <Slider
120
+ aria-label="Price range"
121
+ defaultValue={[3, 9]}
122
+ max={12}
123
+ step={1}
124
+ thumbProps={[
125
+ { "data-thumb": "min" } as SliderThumbProps,
126
+ { "data-thumb": "max" } as SliderThumbProps,
127
+ ]}
128
+ />,
129
+ );
130
+ const [low, high] = screen.getAllByRole("slider");
131
+ expect(low).toHaveAttribute("data-thumb", "min");
132
+ expect(high).toHaveAttribute("data-thumb", "max");
133
+ });
134
+
135
+ it("still supports a single-element defaultValue with the existing object thumbProps shape", () => {
136
+ render(
137
+ <Slider
138
+ aria-label="Volume"
139
+ defaultValue={[50]}
140
+ max={100}
141
+ step={1}
142
+ thumbProps={{ "aria-valuetext": "50%" }}
143
+ />,
144
+ );
145
+ expect(screen.getByRole("slider", { name: "Volume" })).toHaveAttribute(
146
+ "aria-valuetext",
147
+ "50%",
148
+ );
149
+ });
150
+
151
+ it("applies PROTECTED_THUMB_KEYS stripping per-thumb in the array shape", () => {
152
+ render(
153
+ <Slider
154
+ defaultValue={[3, 9]}
155
+ max={12}
156
+ step={1}
157
+ thumbProps={[
158
+ { "aria-label": "Min", role: "presentation", tabIndex: -1 },
159
+ { "aria-label": "Max", "aria-valuenow": 999 },
160
+ ]}
161
+ />,
162
+ );
163
+ const [low, high] = screen.getAllByRole("slider");
164
+ expect(low).toHaveAttribute("tabindex", "0");
165
+ expect(high).toHaveAttribute("aria-valuenow", "9");
166
+ });
167
+ });
168
+ });
@@ -0,0 +1,136 @@
1
+ import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
2
+ import * as SliderPrimitive from "@radix-ui/react-slider";
3
+ import { cn } from "../../lib/cn";
4
+
5
+ /** Props for a single `SliderPrimitive.Thumb` — see `SliderProps["thumbProps"]`. */
6
+ export type SliderThumbProps = ComponentPropsWithoutRef<typeof SliderPrimitive.Thumb>;
7
+
8
+ export type SliderProps = ComponentPropsWithoutRef<typeof SliderPrimitive.Root> & {
9
+ /**
10
+ * Announced instead of the raw numeric value (e.g. `"step 3 of 12"` for a
11
+ * replay scrubber). Forwarded to the thumb alongside `aria-label`/
12
+ * `aria-labelledby` — see the doc comment below for why it has to land
13
+ * there, not on the root.
14
+ */
15
+ "aria-valuetext"?: string;
16
+ /**
17
+ * Escape hatch spread onto the thumb LAST — for `data-*`/`style`/etc, or to
18
+ * deliberately override `aria-label`/`aria-labelledby`/`aria-valuetext`
19
+ * (an explicit key here always wins over the matching top-level prop).
20
+ *
21
+ * The six keys that MAKE the element a slider are ignored on purpose:
22
+ * `role`, `aria-valuenow`, `aria-valuemin`, `aria-valuemax`,
23
+ * `aria-orientation` and `tabIndex`. Radix derives every one of them from
24
+ * the slider's live state and spreads consumer props AFTER its own, so
25
+ * passing them here would silently destroy the widget (a wrong
26
+ * `aria-valuenow` mis-announces the value; `role`/`tabIndex` make it
27
+ * unreachable). Change the VALUE (`value`/`min`/`max`/`orientation`/
28
+ * `disabled`) on `Slider` instead; use `aria-valuetext` to change how the
29
+ * value is spoken.
30
+ *
31
+ * Radix renders one thumb per element of `value`/`defaultValue` — pass a
32
+ * single object to apply the same props to every thumb (the historical
33
+ * single-thumb shape, unchanged), or an ARRAY to give each thumb its own
34
+ * props, indexed positionally against `value`/`defaultValue` (#398). A
35
+ * missing array slot falls back to no per-thumb override for that thumb.
36
+ */
37
+ thumbProps?: SliderThumbProps | SliderThumbProps[];
38
+ };
39
+
40
+ /**
41
+ * Attributes Radix computes for the thumb and then lets consumer props
42
+ * overwrite (`SliderThumbImpl` spreads them before `...thumbProps`). Dropping
43
+ * them here is what keeps the escape hatch from being a foot-gun.
44
+ */
45
+ const PROTECTED_THUMB_KEYS = [
46
+ "role",
47
+ "aria-valuenow",
48
+ "aria-valuemin",
49
+ "aria-valuemax",
50
+ "aria-orientation",
51
+ "tabIndex",
52
+ ] as const;
53
+
54
+ function withoutProtectedKeys(
55
+ thumbProps: SliderThumbProps | undefined,
56
+ ): SliderThumbProps | undefined {
57
+ if (!thumbProps) return thumbProps;
58
+ let safe = thumbProps;
59
+ for (const key of PROTECTED_THUMB_KEYS) {
60
+ if (!(key in safe)) continue;
61
+ const { [key]: _dropped, ...rest } = safe;
62
+ safe = rest;
63
+ }
64
+ return safe;
65
+ }
66
+
67
+ /** `thumbProps` for the thumb at `index` — the shared object for every thumb, or that array slot. */
68
+ function getThumbPropsAt(
69
+ thumbProps: SliderProps["thumbProps"],
70
+ index: number,
71
+ ): SliderThumbProps | undefined {
72
+ if (!thumbProps) return undefined;
73
+ return Array.isArray(thumbProps) ? thumbProps[index] : thumbProps;
74
+ }
75
+
76
+ /**
77
+ * Range input — a single-thumb value by default, or a multi-thumb range
78
+ * slider when `value`/`defaultValue` is an array with more than one element
79
+ * (Radix's own range-slider shape; #398). One `Thumb` is rendered per value.
80
+ *
81
+ * `aria-label` / `aria-labelledby` / `aria-valuetext` are routed to the
82
+ * THUMB(S), not the root: Radix puts `role="slider"` on the thumb, so a name
83
+ * (or value text) on the root would never reach the widget. Pass `aria-label`
84
+ * (or `aria-labelledby`, for a visible `<Label>`) — an unnamed slider is an
85
+ * accessibility failure. For a range slider, pass an ARRAY to `thumbProps` so
86
+ * each thumb gets its own name/valuetext (e.g. "Minimum price"/"Maximum
87
+ * price") — the top-level `aria-label`/`aria-valuetext` apply identically to
88
+ * every thumb, which is rarely what a multi-thumb slider wants.
89
+ */
90
+ export const Slider = forwardRef<ElementRef<typeof SliderPrimitive.Root>, SliderProps>(
91
+ function Slider(
92
+ {
93
+ className,
94
+ "aria-label": ariaLabel,
95
+ "aria-labelledby": ariaLabelledby,
96
+ "aria-valuetext": ariaValueText,
97
+ thumbProps,
98
+ value,
99
+ defaultValue,
100
+ ...props
101
+ },
102
+ ref,
103
+ ) {
104
+ const values = value ?? defaultValue;
105
+ const thumbCount = Array.isArray(values) ? values.length : 1;
106
+ return (
107
+ <SliderPrimitive.Root
108
+ ref={ref}
109
+ className={cn("relative flex w-full touch-none select-none items-center", className)}
110
+ value={value}
111
+ defaultValue={defaultValue}
112
+ {...props}
113
+ >
114
+ <SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-muted">
115
+ <SliderPrimitive.Range className="absolute h-full bg-primary" />
116
+ </SliderPrimitive.Track>
117
+ {Array.from({ length: thumbCount }, (_, index) => {
118
+ const safeThumbProps = withoutProtectedKeys(getThumbPropsAt(thumbProps, index));
119
+ return (
120
+ <SliderPrimitive.Thumb
121
+ key={index}
122
+ aria-label={ariaLabel}
123
+ aria-labelledby={ariaLabelledby}
124
+ aria-valuetext={ariaValueText}
125
+ {...safeThumbProps}
126
+ className={cn(
127
+ "block size-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
128
+ safeThumbProps?.className,
129
+ )}
130
+ />
131
+ );
132
+ })}
133
+ </SliderPrimitive.Root>
134
+ );
135
+ },
136
+ );
@@ -0,0 +1 @@
1
+ export { SliderNumber, type SliderNumberProps } from "./slider-number";
@@ -0,0 +1,123 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect } from "storybook/test";
3
+ import { useState } from "react";
4
+ import { SliderNumber } from "./slider-number";
5
+
6
+ const meta = {
7
+ title: "Forms/SliderNumber",
8
+ component: SliderNumber,
9
+ tags: ["autodocs"],
10
+ args: {
11
+ "aria-label": "Temperature",
12
+ min: 0,
13
+ max: 1,
14
+ step: 0.1,
15
+ },
16
+ argTypes: {
17
+ value: {
18
+ description: 'Controlled value. Pass `null` for "use the provider default".',
19
+ control: "number",
20
+ table: { category: "State" },
21
+ },
22
+ defaultValue: {
23
+ description: "Uncontrolled initial value.",
24
+ control: "number",
25
+ table: { category: "State" },
26
+ },
27
+ min: {
28
+ description: "Minimum allowed value.",
29
+ control: "number",
30
+ table: { category: "Behavior" },
31
+ },
32
+ max: {
33
+ description: "Maximum allowed value.",
34
+ control: "number",
35
+ table: { category: "Behavior" },
36
+ },
37
+ step: {
38
+ description: "Step increment for both the slider and the number input.",
39
+ control: "number",
40
+ table: { category: "Behavior" },
41
+ },
42
+ precision: {
43
+ description: "Decimal places to round to (defaults to the decimals in `step`).",
44
+ control: "number",
45
+ table: { category: "Behavior" },
46
+ },
47
+ resetLabel: {
48
+ description: "Label for the reset-to-provider-default control.",
49
+ control: "text",
50
+ table: { category: "Content" },
51
+ },
52
+ disabled: {
53
+ description: "Disables the whole control.",
54
+ control: "boolean",
55
+ table: { category: "State" },
56
+ },
57
+ onValueChange: {
58
+ description: "Called when the value changes.",
59
+ control: false,
60
+ table: { category: "Behavior" },
61
+ },
62
+ className: {
63
+ description: "Additional CSS classes applied to the root element.",
64
+ control: "text",
65
+ table: { category: "Appearance" },
66
+ },
67
+ },
68
+ } satisfies Meta<typeof SliderNumber>;
69
+ export default meta;
70
+ type Story = StoryObj<typeof meta>;
71
+
72
+ export const Default: Story = {
73
+ args: { defaultValue: 0.7 },
74
+ };
75
+
76
+ /** Starting from "provider default" — the slider thumb renders dimmed/dashed and Reset is disabled. */
77
+ export const ProviderDefault: Story = {
78
+ args: { defaultValue: null },
79
+ play: async ({ canvas }) => {
80
+ await expect(canvas.getByRole("button", { name: "Reset" })).toBeDisabled();
81
+ },
82
+ };
83
+
84
+ export const Controlled: Story = {
85
+ render: () => {
86
+ // eslint-disable-next-line react-hooks/rules-of-hooks
87
+ const [value, setValue] = useState<number | null>(0.5);
88
+ return (
89
+ <div className="flex flex-col gap-2 max-w-md">
90
+ <SliderNumber
91
+ aria-label="Top-p"
92
+ min={0}
93
+ max={1}
94
+ step={0.05}
95
+ value={value}
96
+ onValueChange={setValue}
97
+ />
98
+ <p className="text-body text-muted-foreground">Value: {value ?? "provider default"}</p>
99
+ </div>
100
+ );
101
+ },
102
+ };
103
+
104
+ /** Dragging (via keyboard) to a value, then resetting back to the provider default. */
105
+ export const DragThenReset: Story = {
106
+ args: { defaultValue: null },
107
+ play: async ({ canvas, userEvent }) => {
108
+ const thumb = canvas.getByRole("slider", { name: "Temperature" });
109
+ thumb.focus();
110
+ await userEvent.keyboard("{ArrowRight}{ArrowRight}{ArrowRight}");
111
+ const numberInput = canvas.getByRole("spinbutton", { name: "Temperature" });
112
+ await expect(numberInput).toHaveValue(0.3);
113
+
114
+ const resetBtn = canvas.getByRole("button", { name: "Reset" });
115
+ await expect(resetBtn).not.toBeDisabled();
116
+ await userEvent.click(resetBtn);
117
+ await expect(resetBtn).toBeDisabled();
118
+ },
119
+ };
120
+
121
+ export const Disabled: Story = {
122
+ args: { defaultValue: 0.4, disabled: true },
123
+ };