@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,314 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import type { TreeNode } from "./tree";
5
+
6
+ /**
7
+ * Flattens the visible (expanded) tree into an ordered list of node ids.
8
+ * Nodes whose ancestors are collapsed are excluded.
9
+ */
10
+ export function flattenVisible<T>(nodes: TreeNode<T>[], expandedIds: Set<string>): string[] {
11
+ const result: string[] = [];
12
+ function walk(list: TreeNode<T>[]) {
13
+ for (const node of list) {
14
+ result.push(node.id);
15
+ if ((node.children?.length || node.hasChildren) && expandedIds.has(node.id)) {
16
+ if (node.children?.length) walk(node.children);
17
+ }
18
+ }
19
+ }
20
+ walk(nodes);
21
+ return result;
22
+ }
23
+
24
+ /**
25
+ * Builds a lookup map: node id → parent id (or null for roots).
26
+ */
27
+ function buildParentMap<T>(
28
+ nodes: TreeNode<T>[],
29
+ parentId: string | null = null,
30
+ map: Map<string, string | null> = new Map(),
31
+ ): Map<string, string | null> {
32
+ for (const node of nodes) {
33
+ map.set(node.id, parentId);
34
+ if (node.children?.length) {
35
+ buildParentMap(node.children, node.id, map);
36
+ }
37
+ }
38
+ return map;
39
+ }
40
+
41
+ /**
42
+ * Finds a node by id in the tree (depth-first).
43
+ */
44
+ export function findNode<T>(nodes: TreeNode<T>[], id: string): TreeNode<T> | undefined {
45
+ for (const node of nodes) {
46
+ if (node.id === id) return node;
47
+ if (node.children?.length) {
48
+ const found = findNode(node.children, id);
49
+ if (found) return found;
50
+ }
51
+ }
52
+ return undefined;
53
+ }
54
+
55
+ export interface UseTreeKeyboardOptions<T> {
56
+ nodes: TreeNode<T>[];
57
+ expandedIds: Set<string>;
58
+ onExpandedChange: (ids: Set<string>) => void;
59
+ selectionMode: "single" | "multiple" | "none";
60
+ selectedIds: Set<string>;
61
+ onSelectionChange: (ids: Set<string>) => void;
62
+ /**
63
+ * When `true`, the tree uses windowed rendering. Focus navigation must
64
+ * scroll the virtualizer before focusing an unmounted row.
65
+ */
66
+ virtualize?: boolean;
67
+ /**
68
+ * Ordered list of visible node ids in the virtual flat list. Provided by
69
+ * the Tree when `virtualize` is true so the keyboard hook can map an id to
70
+ * a virtual index without re-flattening.
71
+ */
72
+ flatNodeIds?: string[];
73
+ /**
74
+ * Ref that the keyboard hook writes a node id into when it needs to focus a
75
+ * row that is currently scrolled out of view. The Tree's `registerAndFlush`
76
+ * callback reads this ref on every ref registration and focuses the element
77
+ * once it mounts.
78
+ */
79
+ pendingFocusIdRef?: React.MutableRefObject<string | null>;
80
+ /**
81
+ * Called when the keyboard hook wants to bring an index into view before
82
+ * focusing it. Only called in virtual mode.
83
+ */
84
+ scrollToVirtualIndex?: (index: number) => void;
85
+ }
86
+
87
+ export interface UseTreeKeyboardResult {
88
+ activeId: string | null;
89
+ setActiveId: (id: string | null) => void;
90
+ /** ref callback — register each tree item element keyed by node id */
91
+ registerNodeRef: (id: string, el: HTMLElement | null) => void;
92
+ /** onKeyDown handler to attach to the root tree div */
93
+ onKeyDown: (e: React.KeyboardEvent<HTMLElement>) => void;
94
+ }
95
+
96
+ export function useTreeKeyboard<T>(options: UseTreeKeyboardOptions<T>): UseTreeKeyboardResult {
97
+ const {
98
+ nodes,
99
+ expandedIds,
100
+ onExpandedChange,
101
+ selectionMode,
102
+ selectedIds,
103
+ onSelectionChange,
104
+ virtualize = false,
105
+ flatNodeIds,
106
+ pendingFocusIdRef,
107
+ scrollToVirtualIndex,
108
+ } = options;
109
+
110
+ // The currently "roving" active item id
111
+ const [activeId, setActiveIdState] = useState<string | null>(() => {
112
+ // default to first visible node
113
+ const visible = flattenVisible(nodes, expandedIds);
114
+ return visible[0] ?? null;
115
+ });
116
+
117
+ // Map from node id → DOM element reference
118
+ const nodeRefs = useRef<Map<string, HTMLElement>>(new Map());
119
+
120
+ const registerNodeRef = useCallback((id: string, el: HTMLElement | null) => {
121
+ if (el) {
122
+ nodeRefs.current.set(id, el);
123
+ } else {
124
+ nodeRefs.current.delete(id);
125
+ }
126
+ }, []);
127
+
128
+ // Typeahead buffer
129
+ const typeaheadBuffer = useRef<string>("");
130
+ const typeaheadTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
131
+
132
+ // Clear any pending typeahead timer on unmount.
133
+ useEffect(
134
+ () => () => {
135
+ if (typeaheadTimer.current) clearTimeout(typeaheadTimer.current);
136
+ },
137
+ [],
138
+ );
139
+
140
+ /**
141
+ * Focus the element for `id`. In virtual mode, if the element isn't
142
+ * mounted yet, record it as pending and scroll the virtualizer so the row
143
+ * enters the render window; the Tree's `registerAndFlush` will focus it
144
+ * once the ref fires.
145
+ */
146
+ const setActiveId = useCallback(
147
+ (id: string | null) => {
148
+ setActiveIdState(id);
149
+ if (!id) return;
150
+
151
+ const el = nodeRefs.current.get(id);
152
+ if (el) {
153
+ el.focus();
154
+ return;
155
+ }
156
+
157
+ // Element not mounted — happens when it's scrolled out of the virtual window.
158
+ if (virtualize && scrollToVirtualIndex && pendingFocusIdRef) {
159
+ const ids = flatNodeIds ?? [];
160
+ const index = ids.indexOf(id);
161
+ if (index >= 0) {
162
+ pendingFocusIdRef.current = id;
163
+ scrollToVirtualIndex(index);
164
+ // The Tree's registerAndFlush will call el.focus() once the row mounts.
165
+ }
166
+ }
167
+ },
168
+ [virtualize, flatNodeIds, pendingFocusIdRef, scrollToVirtualIndex],
169
+ );
170
+
171
+ const onKeyDown = useCallback(
172
+ (e: React.KeyboardEvent<HTMLElement>) => {
173
+ if (!activeId) return;
174
+
175
+ // In virtual mode use the pre-computed flat list so we don't re-flatten
176
+ const visible = virtualize && flatNodeIds ? flatNodeIds : flattenVisible(nodes, expandedIds);
177
+ const currentIndex = visible.indexOf(activeId);
178
+ const currentNode = findNode(nodes, activeId);
179
+ const parentMap = buildParentMap(nodes);
180
+
181
+ const moveTo = (id: string) => {
182
+ e.preventDefault();
183
+ setActiveId(id);
184
+ };
185
+
186
+ const selectNode = (id: string) => {
187
+ if (selectionMode === "none") return;
188
+ const node = findNode(nodes, id);
189
+ if (node?.disabled) return;
190
+ if (selectionMode === "single") {
191
+ onSelectionChange(new Set([id]));
192
+ } else {
193
+ const next = new Set(selectedIds);
194
+ if (next.has(id)) {
195
+ next.delete(id);
196
+ } else {
197
+ next.add(id);
198
+ }
199
+ onSelectionChange(next);
200
+ }
201
+ };
202
+
203
+ switch (e.key) {
204
+ case "ArrowDown": {
205
+ e.preventDefault();
206
+ const next = visible[currentIndex + 1];
207
+ if (next) moveTo(next);
208
+ break;
209
+ }
210
+ case "ArrowUp": {
211
+ e.preventDefault();
212
+ const prev = visible[currentIndex - 1];
213
+ if (prev) moveTo(prev);
214
+ break;
215
+ }
216
+ case "ArrowRight": {
217
+ e.preventDefault();
218
+ if (!currentNode) break;
219
+ const expandable = !!(currentNode.children?.length || currentNode.hasChildren);
220
+ if (expandable && !expandedIds.has(activeId)) {
221
+ // expand
222
+ const next = new Set(expandedIds);
223
+ next.add(activeId);
224
+ onExpandedChange(next);
225
+ } else if (expandable && expandedIds.has(activeId)) {
226
+ // move to first child (only if loaded)
227
+ const firstChild = currentNode.children?.[0];
228
+ if (firstChild) moveTo(firstChild.id);
229
+ }
230
+ // leaf: do nothing
231
+ break;
232
+ }
233
+ case "ArrowLeft": {
234
+ e.preventDefault();
235
+ if (!currentNode) break;
236
+ const expandable = !!(currentNode.children?.length || currentNode.hasChildren);
237
+ if (expandable && expandedIds.has(activeId)) {
238
+ // collapse
239
+ const next = new Set(expandedIds);
240
+ next.delete(activeId);
241
+ onExpandedChange(next);
242
+ } else {
243
+ // move to parent
244
+ const parentId = parentMap.get(activeId) ?? null;
245
+ if (parentId) moveTo(parentId);
246
+ }
247
+ break;
248
+ }
249
+ case "Home": {
250
+ e.preventDefault();
251
+ const first = visible[0];
252
+ if (first) moveTo(first);
253
+ break;
254
+ }
255
+ case "End": {
256
+ e.preventDefault();
257
+ const last = visible[visible.length - 1];
258
+ if (last) moveTo(last);
259
+ break;
260
+ }
261
+ case "Enter":
262
+ case " ": {
263
+ e.preventDefault();
264
+ selectNode(activeId);
265
+ break;
266
+ }
267
+ default: {
268
+ // typeahead — single printable characters
269
+ if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
270
+ const char = e.key.toLowerCase();
271
+ typeaheadBuffer.current += char;
272
+
273
+ if (typeaheadTimer.current) clearTimeout(typeaheadTimer.current);
274
+ typeaheadTimer.current = setTimeout(() => {
275
+ typeaheadBuffer.current = "";
276
+ }, 500);
277
+
278
+ const prefix = typeaheadBuffer.current;
279
+ // find the next visible node (after current) whose label starts with prefix
280
+ const searchList = [
281
+ ...visible.slice(currentIndex + 1),
282
+ ...visible.slice(0, currentIndex + 1),
283
+ ];
284
+ for (const id of searchList) {
285
+ const node = findNode(nodes, id);
286
+ if (!node) continue;
287
+ // only string labels are searchable
288
+ if (typeof node.label !== "string") continue;
289
+ if (node.label.toLowerCase().startsWith(prefix)) {
290
+ moveTo(id);
291
+ break;
292
+ }
293
+ }
294
+ }
295
+ break;
296
+ }
297
+ }
298
+ },
299
+ [
300
+ activeId,
301
+ nodes,
302
+ expandedIds,
303
+ onExpandedChange,
304
+ selectionMode,
305
+ selectedIds,
306
+ onSelectionChange,
307
+ setActiveId,
308
+ virtualize,
309
+ flatNodeIds,
310
+ ],
311
+ );
312
+
313
+ return { activeId, setActiveId, registerNodeRef, onKeyDown };
314
+ }
@@ -0,0 +1 @@
1
+ export { TreeSelect, type TreeSelectProps } from "./tree-select";
@@ -0,0 +1,127 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, within } from "storybook/test";
3
+ import { useState } from "react";
4
+ import { Folder, FileText } from "lucide-react";
5
+ import { TreeSelect } from "./tree-select";
6
+ import type { TreeNode } from "../tree";
7
+
8
+ const tree: TreeNode[] = [
9
+ {
10
+ id: "engineering",
11
+ label: "Engineering",
12
+ icon: <Folder className="size-4 text-muted-foreground" />,
13
+ children: [
14
+ {
15
+ id: "frontend",
16
+ label: "Frontend",
17
+ icon: <FileText className="size-4 text-muted-foreground" />,
18
+ },
19
+ {
20
+ id: "backend",
21
+ label: "Backend",
22
+ icon: <FileText className="size-4 text-muted-foreground" />,
23
+ },
24
+ {
25
+ id: "platform",
26
+ label: "Platform",
27
+ icon: <FileText className="size-4 text-muted-foreground" />,
28
+ },
29
+ ],
30
+ },
31
+ {
32
+ id: "design",
33
+ label: "Design",
34
+ icon: <Folder className="size-4 text-muted-foreground" />,
35
+ children: [
36
+ {
37
+ id: "product-design",
38
+ label: "Product Design",
39
+ icon: <FileText className="size-4 text-muted-foreground" />,
40
+ },
41
+ {
42
+ id: "research",
43
+ label: "Research",
44
+ icon: <FileText className="size-4 text-muted-foreground" />,
45
+ },
46
+ ],
47
+ },
48
+ ];
49
+
50
+ const meta = {
51
+ title: "Forms/TreeSelect",
52
+ component: TreeSelect,
53
+ tags: ["autodocs"],
54
+ argTypes: {
55
+ nodes: {
56
+ description: "Hierarchical tree data (`TreeNode[]`).",
57
+ control: false,
58
+ table: { category: "Content" },
59
+ },
60
+ value: {
61
+ description: "Controlled selection — `string` (single) or `string[]` (multiple).",
62
+ control: false,
63
+ table: { category: "State" },
64
+ },
65
+ defaultValue: {
66
+ description: "Uncontrolled initial selection.",
67
+ control: false,
68
+ table: { category: "State" },
69
+ },
70
+ multiple: {
71
+ description: "Enable multi-select (checkbox tree).",
72
+ control: "boolean",
73
+ table: { category: "Behavior" },
74
+ },
75
+ placeholder: {
76
+ description: "Trigger text when nothing is selected.",
77
+ control: "text",
78
+ table: { category: "Content" },
79
+ },
80
+ onValueChange: {
81
+ description: "Called when the selection changes.",
82
+ control: false,
83
+ table: { category: "Behavior" },
84
+ },
85
+ className: {
86
+ description: "Additional CSS classes applied to the trigger button.",
87
+ control: "text",
88
+ table: { category: "Appearance" },
89
+ },
90
+ },
91
+ } satisfies Meta<typeof TreeSelect>;
92
+
93
+ export default meta;
94
+ type Story = StoryObj<typeof meta>;
95
+
96
+ export const Default: Story = {
97
+ args: { nodes: tree, placeholder: "Select a team" },
98
+ // Opens the tree popover and confirms root nodes are visible.
99
+ play: async ({ canvas, canvasElement, userEvent }) => {
100
+ const trigger = canvas.getByRole("combobox");
101
+ await userEvent.click(trigger);
102
+ const body = within(canvasElement.ownerDocument.body);
103
+ const engineeringNode = await body.findByRole("treeitem", { name: /engineering/i });
104
+ await expect(engineeringNode).toBeInTheDocument();
105
+ },
106
+ };
107
+
108
+ export const Preselected: Story = {
109
+ args: { nodes: tree, defaultValue: "frontend" },
110
+ };
111
+
112
+ function MultipleExample() {
113
+ const [value, setValue] = useState<string[]>(["frontend", "research"]);
114
+ return (
115
+ <TreeSelect
116
+ nodes={tree}
117
+ multiple
118
+ value={value}
119
+ onValueChange={(v) => setValue(v as string[])}
120
+ placeholder="Select teams"
121
+ />
122
+ );
123
+ }
124
+
125
+ export const Multiple: Story = {
126
+ render: () => <MultipleExample />,
127
+ };
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { TreeSelect } from "./tree-select";
4
+ import type { TreeNode } from "../tree";
5
+
6
+ const nodes: TreeNode[] = [
7
+ {
8
+ id: "fruits",
9
+ label: "Fruits",
10
+ children: [
11
+ { id: "apple", label: "Apple" },
12
+ { id: "banana", label: "Banana" },
13
+ ],
14
+ },
15
+ { id: "veggies", label: "Vegetables", children: [{ id: "carrot", label: "Carrot" }] },
16
+ ];
17
+
18
+ describe("TreeSelect", () => {
19
+ it("renders the placeholder on the trigger when nothing is selected", () => {
20
+ render(<TreeSelect nodes={nodes} placeholder="Pick a node" />);
21
+ expect(screen.getByText("Pick a node")).toBeInTheDocument();
22
+ expect(screen.getByRole("combobox")).toHaveAttribute("aria-expanded", "false");
23
+ });
24
+
25
+ it("shows the selected label for a controlled single value", () => {
26
+ render(<TreeSelect nodes={nodes} value="apple" />);
27
+ expect(screen.getByText("Apple")).toBeInTheDocument();
28
+ });
29
+
30
+ it("renders a badge per selected node in multiple mode", () => {
31
+ render(<TreeSelect nodes={nodes} multiple value={["apple", "carrot"]} />);
32
+ expect(screen.getByText("Apple")).toBeInTheDocument();
33
+ expect(screen.getByText("Carrot")).toBeInTheDocument();
34
+ });
35
+
36
+ it("collapses overflow badges into a +N counter", () => {
37
+ render(<TreeSelect nodes={nodes} multiple value={["apple", "banana", "carrot"]} />);
38
+ expect(screen.getByText("+1")).toBeInTheDocument();
39
+ });
40
+ });
@@ -0,0 +1,137 @@
1
+ // a2ui.exposed: yes
2
+ "use client";
3
+
4
+ import { forwardRef, useId, useState, type ButtonHTMLAttributes, type ReactNode } from "react";
5
+ import { ChevronsUpDown } from "lucide-react";
6
+ import { cn } from "../../lib/cn";
7
+ import { Badge } from "../badge";
8
+ import { Button } from "../button";
9
+ import { Popover, PopoverContent, PopoverTrigger } from "../popover";
10
+ import { Tree, type TreeNode } from "../tree";
11
+
12
+ export interface TreeSelectProps<T = unknown> extends Omit<
13
+ ButtonHTMLAttributes<HTMLButtonElement>,
14
+ "value" | "onChange"
15
+ > {
16
+ nodes: TreeNode<T>[];
17
+ /** Controlled value — `string` in single mode, `string[]` in multiple mode. */
18
+ value?: string | string[];
19
+ /** Uncontrolled initial value. */
20
+ defaultValue?: string | string[];
21
+ onValueChange?: (value: string | string[]) => void;
22
+ /** Allow selecting multiple nodes (checkbox tree). Default false. */
23
+ multiple?: boolean;
24
+ placeholder?: string;
25
+ className?: string;
26
+ }
27
+
28
+ function toArray(v: string | string[] | undefined): string[] {
29
+ if (v === undefined) return [];
30
+ if (Array.isArray(v)) return v;
31
+ return v === "" ? [] : [v];
32
+ }
33
+
34
+ function findLabel<T>(nodes: TreeNode<T>[], id: string): ReactNode | undefined {
35
+ for (const node of nodes) {
36
+ if (node.id === id) return node.label;
37
+ if (node.children?.length) {
38
+ const found = findLabel(node.children, id);
39
+ if (found !== undefined) return found;
40
+ }
41
+ }
42
+ return undefined;
43
+ }
44
+
45
+ /**
46
+ * A select whose options are a hierarchical {@link Tree}. Single or multiple
47
+ * selection; composes `Tree` for all keyboard/SR behaviour.
48
+ */
49
+ export const TreeSelect = forwardRef<HTMLButtonElement, TreeSelectProps>(function TreeSelect(
50
+ {
51
+ nodes,
52
+ value,
53
+ defaultValue,
54
+ onValueChange,
55
+ multiple = false,
56
+ placeholder = "Select…",
57
+ className,
58
+ ...props
59
+ },
60
+ ref,
61
+ ) {
62
+ const [open, setOpen] = useState(false);
63
+ const treeId = useId();
64
+ const isControlled = value !== undefined;
65
+ const [internal, setInternal] = useState<string[]>(() => toArray(defaultValue));
66
+ const selected = isControlled ? toArray(value) : internal;
67
+
68
+ const emit = (next: string[]) => {
69
+ if (!isControlled) setInternal(next);
70
+ onValueChange?.(multiple ? next : (next[0] ?? ""));
71
+ };
72
+
73
+ const handleSelectionChange = (ids: string[]) => {
74
+ emit(ids);
75
+ if (!multiple) setOpen(false);
76
+ };
77
+
78
+ const labels = selected.map((id) => ({ id, label: findLabel(nodes, id) ?? id }));
79
+
80
+ return (
81
+ <Popover open={open} onOpenChange={setOpen}>
82
+ <PopoverTrigger asChild>
83
+ <Button
84
+ ref={ref}
85
+ type="button"
86
+ variant="outline"
87
+ role="combobox"
88
+ aria-haspopup="tree"
89
+ aria-expanded={open}
90
+ aria-controls={treeId}
91
+ className={cn("h-auto min-h-9 w-64 justify-between font-normal", className)}
92
+ {...props}
93
+ >
94
+ <span className="flex min-w-0 flex-1 flex-wrap items-center gap-1 text-start">
95
+ {labels.length === 0 ? (
96
+ <span className="text-muted-foreground">{placeholder}</span>
97
+ ) : multiple ? (
98
+ <>
99
+ {labels.slice(0, 2).map((l) => (
100
+ <Badge
101
+ key={l.id}
102
+ variant="secondary"
103
+ className="max-w-[10rem] truncate border-border"
104
+ >
105
+ {l.label}
106
+ </Badge>
107
+ ))}
108
+ {labels.length > 2 ? (
109
+ <Badge variant="secondary" aria-label={`and ${labels.length - 2} more selected`}>
110
+ +{labels.length - 2}
111
+ </Badge>
112
+ ) : null}
113
+ </>
114
+ ) : (
115
+ <span className="truncate">{labels[0]?.label}</span>
116
+ )}
117
+ </span>
118
+ <ChevronsUpDown className="ms-2 size-4 shrink-0 opacity-50" aria-hidden="true" />
119
+ </Button>
120
+ </PopoverTrigger>
121
+ <PopoverContent className="w-64 p-1" align="start">
122
+ <Tree
123
+ id={treeId}
124
+ nodes={nodes}
125
+ selectionMode={multiple ? "multiple" : "single"}
126
+ checkboxes={multiple}
127
+ selectedIds={selected}
128
+ onSelectionChange={handleSelectionChange}
129
+ defaultExpandedIds={nodes.map((n) => n.id)}
130
+ className="max-h-72 overflow-auto"
131
+ />
132
+ </PopoverContent>
133
+ </Popover>
134
+ );
135
+ }) as <T = unknown>(
136
+ props: TreeSelectProps<T> & { ref?: React.Ref<HTMLButtonElement> },
137
+ ) => React.ReactElement | null;
@@ -0,0 +1,31 @@
1
+ export {
2
+ Heading,
3
+ Text,
4
+ headingVariants,
5
+ textVariants,
6
+ TEXT_ROLE_REM,
7
+ type HeadingProps,
8
+ type HeadingLevel,
9
+ type HeadingSize,
10
+ type TextProps,
11
+ type TextVariant,
12
+ type TextTone,
13
+ type TextRole,
14
+ } from "./typography";
15
+ export {
16
+ ProseHeading,
17
+ ProseText,
18
+ ProseLink,
19
+ ProseList,
20
+ ProseListItem,
21
+ ProseBlockquote,
22
+ ProseInlineCode,
23
+ PROSE_HEADING_REM,
24
+ PROSE_HEADING_WEIGHT,
25
+ PROSE_HEADING_TRACKING,
26
+ type ProseHeadingProps,
27
+ type ProseHeadingLevel,
28
+ type ProseTextProps,
29
+ type ProseLinkProps,
30
+ type ProseListProps,
31
+ } from "./prose";
@@ -0,0 +1,48 @@
1
+ /**
2
+ * prose.test.tsx — #399 class-name lock on the on-surface brand-accent rung.
3
+ *
4
+ * These are CLASS-NAME locks, NOT contrast proofs. The ratio itself is asserted
5
+ * on the tokens in `packages/tokens/src/themes-contrast.test.ts`
6
+ * (`primary-text ≥ 4.5:1 on …`, every theme × five content surfaces); what this
7
+ * file pins is that the components that render the brand accent AS TEXT keep
8
+ * reaching for that rung instead of sliding back onto the `--primary` FILL,
9
+ * whose gated contract is only the WCAG 1.4.11 mark bar (3:1). The two halves
10
+ * together are what makes #399 non-regressable: a token that clears AA nobody
11
+ * uses fixes nothing, and a call site pointing at a token nobody gates is how
12
+ * this shipped sub-AA in the first place.
13
+ */
14
+ import { cleanup, render, screen } from "@testing-library/react";
15
+ import { afterEach, describe, expect, it } from "vitest";
16
+
17
+ import { Text } from "./typography";
18
+ import { ProseLink } from "./prose";
19
+
20
+ afterEach(cleanup);
21
+
22
+ describe("ProseLink — #399 on-surface brand-accent rung", () => {
23
+ it("renders text-primary-text, never the text-primary fill rung", () => {
24
+ render(<ProseLink href="https://example.com">docs</ProseLink>);
25
+
26
+ const link = screen.getByRole("link", { name: "docs" });
27
+ const classes = link.className.split(/\s+/);
28
+ expect(classes).toContain("text-primary-text");
29
+ expect(classes).not.toContain("text-primary");
30
+ });
31
+
32
+ it("keeps its resting underline (the non-colour cue is independent of the rung)", () => {
33
+ render(<ProseLink href="https://example.com">docs</ProseLink>);
34
+
35
+ const classes = screen.getByRole("link", { name: "docs" }).className.split(/\s+/);
36
+ expect(classes).toContain("underline");
37
+ });
38
+ });
39
+
40
+ describe("Text tone=primary — #399 on-surface brand-accent rung", () => {
41
+ it("maps the primary tone to text-primary-text, never the fill rung", () => {
42
+ render(<Text tone="primary">accent</Text>);
43
+
44
+ const classes = screen.getByText("accent").className.split(/\s+/);
45
+ expect(classes).toContain("text-primary-text");
46
+ expect(classes).not.toContain("text-primary");
47
+ });
48
+ });