@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,188 @@
1
+ /**
2
+ * Timeline — THE rail/node/connector spine (#190, research 10 §B.2): an
3
+ * ordered list of steps with a vertical connector and a status-colored node
4
+ * per step. Moved from `@elabs-ai/components-editor` (which re-exports it for back-compat —
5
+ * the ADR-0012 own/re-export model); the branded target for the editor's
6
+ * `:::timeline` markdown directive, and reusable anywhere a process/sequence
7
+ * needs showing. Token-driven, theme-safe, motion-free (animation belongs to
8
+ * composing consumers, e.g. the `@elabs-ai/components-ai` agent grammar).
9
+ *
10
+ * One rail, two front doors:
11
+ * - compound `TimelineRoot` + `TimelineItem` — for composing consumers that
12
+ * interleave rich children per node. The node takes the CANONICAL 7-state
13
+ * `Status` (status-badge, #189) — closed enum, no freeform strings.
14
+ * - array `Timeline items={TimelineEntry[]}` — a thin map over the compound
15
+ * parts. Keeps the editor-facing `done|active|pending` API unchanged by
16
+ * mapping in via `fromTimelineStatus` at the boundary.
17
+ *
18
+ * Fork-prevention: a Timeline re-implementation outside this folder fails
19
+ * `pnpm timeline-fork:check` (scripts/check-timeline-fork.mjs).
20
+ */
21
+ import { forwardRef, type HTMLAttributes, type LiHTMLAttributes, type ReactNode } from "react";
22
+ import { cn } from "../../lib/cn";
23
+ import {
24
+ fromTimelineStatus,
25
+ STATUS_LABELS,
26
+ type Status,
27
+ type TimelineStatus,
28
+ } from "../status-badge";
29
+
30
+ /**
31
+ * Node visual per canonical status. This is a DUPLICATE of the status→role
32
+ * mapping `status-badge.tsx` owns (`STATUS_ROLE`) — it exists because the rail
33
+ * node is a filled dot, not a badge pill, so it can't share `statusBadgeVariants`
34
+ * directly. A duplicate is only safe when it is TESTED against the source of
35
+ * truth: `timeline.test.tsx` asserts every chromatic status here resolves to
36
+ * the same role as `STATUS_ROLE`/`statusBadgeVariants` (#392 — `running` had
37
+ * silently drifted to `--primary` here while the canonical map said `--info`,
38
+ * because nothing checked the two agreed). Semantic tokens only.
39
+ *
40
+ * Every status ALSO carries a non-colour signature (#387, WCAG 1.4.1) — a
41
+ * 12px dot can't host a legible glyph, so the cue is geometric. It took an
42
+ * empirical (rendered) pass to find a signature that actually PAINTS
43
+ * distinctly, not merely one that differs as a class string:
44
+ * - `denied`/`skipped` (hollow-ish `bg-muted`, `border-border`) get
45
+ * `border-dashed`/`border-dotted` — this WORKS because border and fill
46
+ * are different colours there, so the gaps in the dash/dot pattern show
47
+ * the (lighter) fill through the (darker) border.
48
+ * - `running`/`complete`/`awaiting-approval`/`failed` are all `bg-<status>
49
+ * border-<status>` — the vivid MARK rung, where border and fill are the
50
+ * SAME token. A `border-style` on a same-colour border is invisible (no
51
+ * colour boundary for the dash gaps to reveal), so this quartet is
52
+ * differentiated by `ring-*` WIDTH instead — the ring sits outside the
53
+ * dot, contrasting against the PAGE, not the fill, which is why
54
+ * `running`'s existing halo was legible in the first place. Widths
55
+ * `0 / 1 / 2 / 4` give four unmistakably different silhouettes:
56
+ * `complete` none (calm/settled, matches `StatusBadge`'s quiet
57
+ * treatment for `complete`) → `awaiting-approval` `ring-1` (a nudge) →
58
+ * `running` `ring-2` (active) → `failed` `ring-4` (the widest, most
59
+ * emphatic — the state that most needs to grab the eye).
60
+ * `denied`/`skipped` were byte-identical before this; the ring-width
61
+ * progression replaces an earlier border-style-only attempt that turned out
62
+ * to be visually void on the vivid quartet — verified in `timeline.test.tsx`
63
+ * (structural) and by rendered zoomed + greyscale screenshots (see #387
64
+ * rendered proof) — don't reintroduce border-style there without re-checking
65
+ * a render, it will look correct in code and disappear on screen.
66
+ */
67
+ export const NODE_STYLE: Record<Status, string> = {
68
+ pending: "border-border bg-background",
69
+ running: "border-info bg-info ring-2 ring-info/25",
70
+ complete: "border-success bg-success",
71
+ "awaiting-approval": "border-warning bg-warning ring-1 ring-warning/40",
72
+ denied: "border-border bg-muted border-dashed",
73
+ failed: "border-destructive bg-destructive ring-4 ring-destructive/20",
74
+ skipped: "border-border bg-muted border-dotted",
75
+ };
76
+
77
+ /** Statuses whose title reads de-emphasized (not-yet/never-run steps). */
78
+ const MUTED_TITLE_STATUSES: ReadonlySet<Status> = new Set(["pending", "skipped", "denied"]);
79
+
80
+ export type TimelineRootProps = HTMLAttributes<HTMLOListElement>;
81
+
82
+ /** The `<ol>` that owns the rail geometry; compose `TimelineItem`s inside. */
83
+ export const TimelineRoot = forwardRef<HTMLOListElement, TimelineRootProps>(function TimelineRoot(
84
+ { className, ...props },
85
+ ref,
86
+ ) {
87
+ return <ol ref={ref} className={cn("relative", className)} {...props} />;
88
+ });
89
+
90
+ export interface TimelineItemProps extends LiHTMLAttributes<HTMLLIElement> {
91
+ /** The canonical execution status (closed 7-state enum, status-badge #189). */
92
+ status?: Status;
93
+ /** Right-aligned meta (e.g. a date) on the title row. */
94
+ timestamp?: ReactNode;
95
+ /** Secondary line under the title. */
96
+ description?: ReactNode;
97
+ /**
98
+ * Rich block content under the title/description, indented with the rail —
99
+ * the slot composing consumers (e.g. `@elabs-ai/components-ai`'s `AgentStep`, #192)
100
+ * interleave per node. Block-level, so it must NOT ride the inline title
101
+ * slot (`children`).
102
+ */
103
+ detail?: ReactNode;
104
+ }
105
+
106
+ /**
107
+ * One `<li>`: status node + connector + the title row; `children` is the
108
+ * title slot. The connector hides itself on the last item via CSS
109
+ * (`group-last`), so streaming/compound consumers never track "is last".
110
+ */
111
+ export const TimelineItem = forwardRef<HTMLLIElement, TimelineItemProps>(function TimelineItem(
112
+ { status = "pending", timestamp, description, detail, className, children, ...props },
113
+ ref,
114
+ ) {
115
+ return (
116
+ <li
117
+ ref={ref}
118
+ data-status={status}
119
+ className={cn("group/timeline-item relative pb-5 ps-7 last:pb-0", className)}
120
+ {...props}
121
+ >
122
+ <span
123
+ aria-hidden="true"
124
+ className="absolute start-[5px] top-2.5 h-full w-px bg-border group-last/timeline-item:hidden"
125
+ />
126
+ <span
127
+ aria-hidden="true"
128
+ className={cn("absolute start-0 top-1 size-3 rounded-full border-2", NODE_STYLE[status])}
129
+ />
130
+ <div className="flex items-baseline justify-between gap-3">
131
+ <span
132
+ className={cn(
133
+ "text-body font-medium",
134
+ MUTED_TITLE_STATUSES.has(status) ? "text-muted-foreground" : "text-foreground",
135
+ )}
136
+ >
137
+ {/* The dot is `aria-hidden` and `data-status` isn't AT-visible (#387) —
138
+ this is the step's only announced status; kept inline so it reads
139
+ naturally as "<Status>: <title>" instead of a second live region. */}
140
+ <span className="sr-only">{STATUS_LABELS[status]}: </span>
141
+ {children}
142
+ </span>
143
+ {timestamp ? (
144
+ <span className="shrink-0 text-meta text-muted-foreground">{timestamp}</span>
145
+ ) : null}
146
+ </div>
147
+ {description ? <p className="mt-0.5 text-body text-muted-foreground">{description}</p> : null}
148
+ {detail ? <div className="mt-2 space-y-2">{detail}</div> : null}
149
+ </li>
150
+ );
151
+ });
152
+
153
+ /**
154
+ * One step of the array-API `Timeline` (the editor's original item shape —
155
+ * re-exported by `@elabs-ai/components-editor` as `TimelineItem` for back-compat).
156
+ */
157
+ export interface TimelineEntry {
158
+ title: ReactNode;
159
+ description?: ReactNode;
160
+ /** The editor's 3-state vocabulary; mapped in via `fromTimelineStatus`. */
161
+ status?: TimelineStatus;
162
+ timestamp?: ReactNode;
163
+ }
164
+
165
+ export interface TimelineProps extends HTMLAttributes<HTMLOListElement> {
166
+ items: TimelineEntry[];
167
+ }
168
+
169
+ /** Array convenience over the compound parts (data in, list out). */
170
+ export const Timeline = forwardRef<HTMLOListElement, TimelineProps>(function Timeline(
171
+ { items, className, ...props },
172
+ ref,
173
+ ) {
174
+ return (
175
+ <TimelineRoot ref={ref} className={className} {...props}>
176
+ {items.map((item, i) => (
177
+ <TimelineItem
178
+ key={i}
179
+ status={fromTimelineStatus(item.status ?? "pending")}
180
+ timestamp={item.timestamp}
181
+ description={item.description}
182
+ >
183
+ {item.title}
184
+ </TimelineItem>
185
+ ))}
186
+ </TimelineRoot>
187
+ );
188
+ });
@@ -0,0 +1 @@
1
+ export { Toggle, toggleVariants } from "./toggle";
@@ -0,0 +1,116 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect } from "storybook/test";
3
+ import { Bold } from "lucide-react";
4
+ import { Toggle } from "./toggle";
5
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../tooltip";
6
+ const meta = {
7
+ title: "Forms/Toggle",
8
+ component: Toggle,
9
+ tags: ["autodocs"],
10
+ argTypes: {
11
+ variant: {
12
+ description: "Visual style of the toggle.",
13
+ control: { type: "select" },
14
+ options: ["default", "outline", "segmented"],
15
+ table: { category: "Appearance" },
16
+ },
17
+ size: {
18
+ description: "Size of the toggle button.",
19
+ control: { type: "radio" },
20
+ options: ["default", "sm", "lg"],
21
+ table: { category: "Appearance" },
22
+ },
23
+ pressed: {
24
+ description: "Controlled pressed state.",
25
+ control: "boolean",
26
+ table: { category: "State" },
27
+ },
28
+ defaultPressed: {
29
+ description: "Uncontrolled initial pressed state.",
30
+ control: "boolean",
31
+ table: { category: "State" },
32
+ },
33
+ disabled: {
34
+ description: "Disables the toggle.",
35
+ control: "boolean",
36
+ table: { category: "State" },
37
+ },
38
+ onPressedChange: {
39
+ description: "Called when the pressed state changes.",
40
+ control: false,
41
+ table: { category: "Behavior" },
42
+ },
43
+ "aria-label": {
44
+ description: "Accessible label (required for icon-only toggles).",
45
+ control: "text",
46
+ table: { category: "Appearance" },
47
+ },
48
+ children: {
49
+ description: "Toggle content — icon and/or text.",
50
+ control: false,
51
+ table: { category: "Content" },
52
+ },
53
+ className: {
54
+ description: "Additional CSS classes applied to the root element.",
55
+ control: "text",
56
+ table: { category: "Appearance" },
57
+ },
58
+ },
59
+ } satisfies Meta<typeof Toggle>;
60
+ export default meta;
61
+ type Story = StoryObj<typeof meta>;
62
+ export const Default: Story = {
63
+ args: { "aria-label": "Bold", children: <Bold /> },
64
+ // Clicks the toggle and confirms the pressed state flips.
65
+ play: async ({ canvas, userEvent }) => {
66
+ const btn = canvas.getByRole("button", { name: /bold/i });
67
+ await expect(btn).toHaveAttribute("aria-pressed", "false");
68
+ await userEvent.click(btn);
69
+ await expect(btn).toHaveAttribute("aria-pressed", "true");
70
+ },
71
+ };
72
+ export const Pressed: Story = {
73
+ args: { "aria-label": "Bold", children: <Bold />, pressed: true },
74
+ };
75
+ export const Outline: Story = { args: { variant: "outline", children: "Bold" } };
76
+ export const OutlinePressed: Story = {
77
+ args: { variant: "outline", children: "Bold", pressed: true },
78
+ };
79
+ // `segmented` was selectable in the controls panel (`argTypes.variant.options`)
80
+ // but never actually rendered by any story (#388), so it never reached the
81
+ // blocking interaction + axe job. This is the mode-switch look — no border, an
82
+ // elevated `bg-surface-elevated` active segment — normally composed inside a
83
+ // `ToggleGroup variant="segmented"` track (see Forms/ToggleGroup → Segmented);
84
+ // rendered standalone here purely to exercise the variant itself.
85
+ export const Segmented: Story = {
86
+ args: { variant: "segmented", "aria-label": "Bold", children: <Bold /> },
87
+ };
88
+ export const Disabled: Story = {
89
+ args: { "aria-label": "Bold", children: <Bold />, disabled: true },
90
+ };
91
+
92
+ // Regression story for #214: a pressed Toggle wrapped in TooltipTrigger asChild.
93
+ // The tooltip's data-state="closed" overwrites the toggle's data-state="on", but
94
+ // the active style must still render because it is also keyed on aria-pressed.
95
+ export const TooltipWrappedPressed: Story = {
96
+ render: () => (
97
+ <TooltipProvider>
98
+ <Tooltip>
99
+ <TooltipTrigger asChild>
100
+ <Toggle aria-label="Bold" pressed>
101
+ <Bold />
102
+ </Toggle>
103
+ </TooltipTrigger>
104
+ <TooltipContent>Bold</TooltipContent>
105
+ </Tooltip>
106
+ </TooltipProvider>
107
+ ),
108
+ play: async ({ canvas }) => {
109
+ const btn = canvas.getByRole("button", { name: /bold/i });
110
+ // aria-pressed must survive the TooltipTrigger asChild merge.
111
+ await expect(btn).toHaveAttribute("aria-pressed", "true");
112
+ // aria-keyed class tokens must be present so the active style renders.
113
+ await expect(btn.className).toContain("aria-pressed:bg-accent");
114
+ await expect(btn.className).toContain("aria-pressed:border-primary");
115
+ },
116
+ };
@@ -0,0 +1,119 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { Toggle } from "./toggle";
5
+ import { ToggleGroup, ToggleGroupItem } from "../toggle-group";
6
+
7
+ describe("Toggle", () => {
8
+ it("toggles pressed state", async () => {
9
+ render(<Toggle aria-label="t">B</Toggle>);
10
+ const t = screen.getByRole("button", { name: "t" });
11
+ await userEvent.click(t);
12
+ expect(t).toHaveAttribute("data-state", "on");
13
+ });
14
+
15
+ // Regression guard for #214: when a TooltipTrigger asChild wrapper merges
16
+ // onto the toggle element it overwrites data-state="on" → "closed". The
17
+ // active styles must survive via aria-pressed / aria-checked keying.
18
+ it("carries aria-pressed=true and aria-keyed active classes when pressed, regardless of data-state", () => {
19
+ render(
20
+ <Toggle aria-label="bold" pressed>
21
+ B
22
+ </Toggle>,
23
+ );
24
+ const btn = screen.getByRole("button", { name: "bold" });
25
+
26
+ // aria-pressed must be "true" — set by the Radix Toggle primitive alone,
27
+ // not clobbered by a TooltipTrigger wrapper.
28
+ expect(btn).toHaveAttribute("aria-pressed", "true");
29
+
30
+ // The className must contain the aria-pressed-keyed active utilities so
31
+ // the style applies even when data-state is overwritten to "closed".
32
+ expect(btn.className).toContain("aria-pressed:bg-accent");
33
+ expect(btn.className).toContain("aria-pressed:text-accent-foreground");
34
+ expect(btn.className).toContain("aria-pressed:font-semibold");
35
+ expect(btn.className).toContain("aria-pressed:border-primary");
36
+ });
37
+
38
+ it("carries aria-pressed=true and aria-keyed active classes on outline variant when pressed", () => {
39
+ render(
40
+ <Toggle aria-label="bold" variant="outline" pressed>
41
+ B
42
+ </Toggle>,
43
+ );
44
+ const btn = screen.getByRole("button", { name: "bold" });
45
+
46
+ expect(btn).toHaveAttribute("aria-pressed", "true");
47
+ expect(btn.className).toContain("aria-pressed:bg-accent");
48
+ expect(btn.className).toContain("aria-pressed:text-accent-foreground");
49
+ expect(btn.className).toContain("aria-pressed:font-semibold");
50
+ expect(btn.className).toContain("aria-pressed:border-primary");
51
+ });
52
+
53
+ // Simulate the exact TooltipTrigger collision: data-state is clobbered to
54
+ // "closed" after mount; the aria-pressed attribute (set by the toggle
55
+ // primitive) must remain "true" and the class tokens must still be present.
56
+ it("active style survives data-state being overwritten to closed (TooltipTrigger collision)", () => {
57
+ render(
58
+ <Toggle aria-label="bold" pressed>
59
+ B
60
+ </Toggle>,
61
+ );
62
+ const btn = screen.getByRole("button", { name: "bold" });
63
+
64
+ // Simulate what TooltipTrigger asChild does: clobber data-state.
65
+ btn.setAttribute("data-state", "closed");
66
+
67
+ // aria-pressed is NOT affected by the data-state clobber.
68
+ expect(btn).toHaveAttribute("aria-pressed", "true");
69
+
70
+ // The aria-keyed class tokens must still be present in className — they
71
+ // will activate in the browser via CSS [aria-pressed="true"] selectors.
72
+ expect(btn.className).toContain("aria-pressed:bg-accent");
73
+ expect(btn.className).toContain("aria-pressed:border-primary");
74
+
75
+ // data-state is now "closed" (confirming the collision scenario).
76
+ expect(btn).toHaveAttribute("data-state", "closed");
77
+ });
78
+ });
79
+
80
+ describe("ToggleGroup / ToggleGroupItem (#214 regression)", () => {
81
+ it("selected ToggleGroupItem carries aria-checked=true and aria-keyed active classes", () => {
82
+ render(
83
+ <ToggleGroup type="single" value="a">
84
+ <ToggleGroupItem value="a" aria-label="item-a">
85
+ A
86
+ </ToggleGroupItem>
87
+ </ToggleGroup>,
88
+ );
89
+ const item = screen.getByRole("radio", { name: "item-a" });
90
+
91
+ // ToggleGroupItem sets aria-checked, not aria-pressed.
92
+ expect(item).toHaveAttribute("aria-checked", "true");
93
+
94
+ // aria-checked-keyed active class tokens must be present.
95
+ expect(item.className).toContain("aria-checked:bg-accent");
96
+ expect(item.className).toContain("aria-checked:text-accent-foreground");
97
+ expect(item.className).toContain("aria-checked:font-semibold");
98
+ expect(item.className).toContain("aria-checked:border-primary");
99
+ });
100
+
101
+ it("active style on ToggleGroupItem survives data-state being overwritten to closed", () => {
102
+ render(
103
+ <ToggleGroup type="single" value="a">
104
+ <ToggleGroupItem value="a" aria-label="item-a">
105
+ A
106
+ </ToggleGroupItem>
107
+ </ToggleGroup>,
108
+ );
109
+ const item = screen.getByRole("radio", { name: "item-a" });
110
+
111
+ // Simulate TooltipTrigger asChild clobbering data-state.
112
+ item.setAttribute("data-state", "closed");
113
+
114
+ expect(item).toHaveAttribute("aria-checked", "true");
115
+ expect(item.className).toContain("aria-checked:bg-accent");
116
+ expect(item.className).toContain("aria-checked:border-primary");
117
+ expect(item).toHaveAttribute("data-state", "closed");
118
+ });
119
+ });
@@ -0,0 +1,64 @@
1
+ import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
2
+ import * as TogglePrimitive from "@radix-ui/react-toggle";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { cn } from "../../lib/cn";
5
+
6
+ export const toggleVariants = cva(
7
+ // Pressed (data-[state=on]) reads as a clearly-distinct, AA-safe state: an
8
+ // accent fill + bolded label (accent/accent-foreground is a guaranteed AA
9
+ // pair) plus a solid `primary` border as a ≥3:1 non-text boundary cue. We
10
+ // deliberately do NOT use `bg-primary`/`text-primary-foreground`: under the
11
+ // pre-debrand green palette that pair measured only 3.61:1 in `light`, which
12
+ // fails AA for text labels (#148). The retuned palette flipped
13
+ // `--primary-foreground` to near-black on a light primary and clears AA
14
+ // comfortably, so the CONTRAST argument is spent — the shape stands on design
15
+ // grounds instead: a pressed toggle is a secondary state and an accent fill is
16
+ // the right weight for it. The border carries the strength; the focus ring is
17
+ // untouched.
18
+ //
19
+ // Active styles are keyed on BOTH data-[state=on]: AND aria-pressed:/aria-checked:
20
+ // (belt-and-suspenders, mirroring the `segmented` variant). A TooltipTrigger
21
+ // asChild wrapper overwrites data-state="on" with "closed" — aria-pressed
22
+ // (Toggle) and aria-checked (ToggleGroupItem) are set by the toggle primitive
23
+ // alone and survive the merge. (#214)
24
+ "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors duration-fast ease-standard hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground data-[state=on]:font-semibold aria-pressed:bg-accent aria-pressed:text-accent-foreground aria-pressed:font-semibold aria-checked:bg-accent aria-checked:text-accent-foreground aria-checked:font-semibold [&_svg]:size-4 [&_svg]:shrink-0",
25
+ {
26
+ variants: {
27
+ variant: {
28
+ // `border border-transparent` reserves the 1px so the pressed
29
+ // `border-primary` does not shift layout.
30
+ default:
31
+ "border border-transparent bg-transparent data-[state=on]:border-primary aria-pressed:border-primary aria-checked:border-primary",
32
+ outline:
33
+ "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground data-[state=on]:border-primary aria-pressed:border-primary aria-checked:border-primary",
34
+ // Segmented mode switch: the SAME elevation grammar as TabsTrigger —
35
+ // raised elevated segment on a recessed `bg-muted` track (the track
36
+ // comes from ToggleGroup `variant="segmented"`). No `border-primary`
37
+ // here: within a group the active boundary is the fill/elevation
38
+ // change (1.4.11 redundant boundary), and no weight shift on press
39
+ // (labels would jiggle the segment widths).
40
+ // Keyed off aria-checked/aria-pressed, NOT data-[state=on]: a
41
+ // TooltipTrigger asChild wrapper overwrites the toggle's data-state
42
+ // with the tooltip's ("closed"), silently killing the active style —
43
+ // the aria attributes are set by the toggle primitive alone.
44
+ segmented:
45
+ "border-0 bg-transparent text-muted-foreground hover:bg-transparent hover:text-foreground aria-checked:bg-surface-elevated aria-checked:text-foreground aria-checked:font-medium aria-checked:shadow-sm aria-pressed:bg-surface-elevated aria-pressed:text-foreground aria-pressed:font-medium aria-pressed:shadow-sm data-[state=on]:bg-surface-elevated data-[state=on]:text-foreground data-[state=on]:font-medium data-[state=on]:shadow-sm",
46
+ },
47
+ size: { default: "h-9 px-2.5 min-w-9", sm: "h-8 px-2 min-w-8", lg: "h-10 px-3 min-w-10" },
48
+ },
49
+ defaultVariants: { variant: "default", size: "default" },
50
+ },
51
+ );
52
+
53
+ export const Toggle = forwardRef<
54
+ ElementRef<typeof TogglePrimitive.Root>,
55
+ ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants>
56
+ >(function Toggle({ className, variant, size, ...props }, ref) {
57
+ return (
58
+ <TogglePrimitive.Root
59
+ ref={ref}
60
+ className={cn(toggleVariants({ variant, size }), className)}
61
+ {...props}
62
+ />
63
+ );
64
+ });
@@ -0,0 +1 @@
1
+ export { ToggleGroup, ToggleGroupItem } from "./toggle-group";
@@ -0,0 +1,110 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect } from "storybook/test";
3
+ import { Bold, BookOpen, Italic, PencilLine, Underline } from "lucide-react";
4
+ import { ToggleGroup, ToggleGroupItem } from "./toggle-group";
5
+ const meta = {
6
+ title: "Forms/ToggleGroup",
7
+ component: ToggleGroup,
8
+ tags: ["autodocs"],
9
+ argTypes: {
10
+ type: {
11
+ description: '`"single"` — at most one item active. `"multiple"` — any number active.',
12
+ control: { type: "radio" },
13
+ options: ["single", "multiple"],
14
+ table: { category: "Behavior" },
15
+ },
16
+ variant: {
17
+ description: "Visual style applied to all items in the group.",
18
+ control: { type: "select" },
19
+ options: ["default", "outline", "segmented"],
20
+ table: { category: "Appearance" },
21
+ },
22
+ size: {
23
+ description: "Size applied to all items.",
24
+ control: { type: "radio" },
25
+ options: ["default", "sm", "lg"],
26
+ table: { category: "Appearance" },
27
+ },
28
+ value: {
29
+ description: "Controlled active value(s).",
30
+ control: false,
31
+ table: { category: "State" },
32
+ },
33
+ defaultValue: {
34
+ description: "Uncontrolled initial active value(s).",
35
+ control: false,
36
+ table: { category: "State" },
37
+ },
38
+ disabled: {
39
+ description: "Disables all items.",
40
+ control: "boolean",
41
+ table: { category: "State" },
42
+ },
43
+ onValueChange: {
44
+ description: "Called when the active value(s) change.",
45
+ control: false,
46
+ table: { category: "Behavior" },
47
+ },
48
+ children: {
49
+ description: "ToggleGroupItem elements.",
50
+ control: false,
51
+ table: { category: "Content" },
52
+ },
53
+ className: {
54
+ description: "Additional CSS classes applied to the root element.",
55
+ control: "text",
56
+ table: { category: "Appearance" },
57
+ },
58
+ },
59
+ } satisfies Meta<typeof ToggleGroup>;
60
+ export default meta;
61
+ type Story = StoryObj<typeof meta>;
62
+ export const Default: Story = {
63
+ render: () => (
64
+ <ToggleGroup type="multiple" variant="outline">
65
+ <ToggleGroupItem value="bold" aria-label="Bold">
66
+ <Bold />
67
+ </ToggleGroupItem>
68
+ <ToggleGroupItem value="italic" aria-label="Italic">
69
+ <Italic />
70
+ </ToggleGroupItem>
71
+ <ToggleGroupItem value="underline" aria-label="Underline">
72
+ <Underline />
73
+ </ToggleGroupItem>
74
+ </ToggleGroup>
75
+ ),
76
+ // Clicks "Bold" to activate it, then clicks again to deactivate.
77
+ play: async ({ canvas, userEvent }) => {
78
+ const boldBtn = canvas.getByRole("button", { name: /bold/i });
79
+ await expect(boldBtn).toHaveAttribute("aria-pressed", "false");
80
+ await userEvent.click(boldBtn);
81
+ await expect(boldBtn).toHaveAttribute("aria-pressed", "true");
82
+ await userEvent.click(boldBtn);
83
+ await expect(boldBtn).toHaveAttribute("aria-pressed", "false");
84
+ },
85
+ };
86
+
87
+ /**
88
+ * Segmented mode switch — a single-select group on a recessed `bg-muted`
89
+ * track with a raised elevated active segment: the same elevation grammar
90
+ * as Tabs. Use it for exclusive view/mode controls (Read ↔ Write, editor
91
+ * source/split/wysiwyg) instead of the bordered toggle variants.
92
+ */
93
+ export const Segmented: Story = {
94
+ render: () => (
95
+ <ToggleGroup
96
+ type="single"
97
+ variant="segmented"
98
+ size="sm"
99
+ defaultValue="read"
100
+ aria-label="Document mode"
101
+ >
102
+ <ToggleGroupItem value="read" aria-label="Read">
103
+ <BookOpen /> Read
104
+ </ToggleGroupItem>
105
+ <ToggleGroupItem value="write" aria-label="Write">
106
+ <PencilLine /> Write
107
+ </ToggleGroupItem>
108
+ </ToggleGroup>
109
+ ),
110
+ };
@@ -0,0 +1,59 @@
1
+ import {
2
+ createContext,
3
+ forwardRef,
4
+ useContext,
5
+ type ComponentPropsWithoutRef,
6
+ type ElementRef,
7
+ } from "react";
8
+ import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
9
+ import { type VariantProps } from "class-variance-authority";
10
+ import { cn } from "../../lib/cn";
11
+ import { toggleVariants } from "../toggle";
12
+
13
+ type ToggleVariantProps = VariantProps<typeof toggleVariants>;
14
+ const ToggleGroupContext = createContext<ToggleVariantProps>({
15
+ size: "default",
16
+ variant: "default",
17
+ });
18
+
19
+ export const ToggleGroup = forwardRef<
20
+ ElementRef<typeof ToggleGroupPrimitive.Root>,
21
+ ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & ToggleVariantProps
22
+ >(function ToggleGroup({ className, variant, size, children, ...props }, ref) {
23
+ return (
24
+ <ToggleGroupPrimitive.Root
25
+ ref={ref}
26
+ className={cn(
27
+ "flex items-center gap-1",
28
+ // Segmented groups carry the TabsList track (recessed muted fill);
29
+ // the raised active segment comes from the item variant.
30
+ variant === "segmented" && "gap-0 rounded-lg bg-muted p-1 text-muted-foreground",
31
+ className,
32
+ )}
33
+ {...props}
34
+ >
35
+ <ToggleGroupContext.Provider value={{ variant, size }}>
36
+ {children}
37
+ </ToggleGroupContext.Provider>
38
+ </ToggleGroupPrimitive.Root>
39
+ );
40
+ });
41
+
42
+ export const ToggleGroupItem = forwardRef<
43
+ ElementRef<typeof ToggleGroupPrimitive.Item>,
44
+ ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & ToggleVariantProps
45
+ >(function ToggleGroupItem({ className, children, variant, size, ...props }, ref) {
46
+ const ctx = useContext(ToggleGroupContext);
47
+ return (
48
+ <ToggleGroupPrimitive.Item
49
+ ref={ref}
50
+ className={cn(
51
+ toggleVariants({ variant: ctx.variant ?? variant, size: ctx.size ?? size }),
52
+ className,
53
+ )}
54
+ {...props}
55
+ >
56
+ {children}
57
+ </ToggleGroupPrimitive.Item>
58
+ );
59
+ });