@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,10 @@
1
+ export {
2
+ Select,
3
+ SelectGroup,
4
+ SelectValue,
5
+ SelectTrigger,
6
+ SelectContent,
7
+ SelectLabel,
8
+ SelectItem,
9
+ SelectSeparator,
10
+ } from "./select";
@@ -0,0 +1,106 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, waitFor, within } from "storybook/test";
3
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../tooltip";
4
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
5
+ const meta = { title: "Forms/Select", component: Select, tags: ["autodocs"] } satisfies Meta<
6
+ typeof Select
7
+ >;
8
+ export default meta;
9
+ type Story = StoryObj<typeof meta>;
10
+ export const Default: Story = {
11
+ render: () => (
12
+ <Select defaultValue="prod">
13
+ <SelectTrigger className="w-56">
14
+ <SelectValue placeholder="Environment" />
15
+ </SelectTrigger>
16
+ <SelectContent>
17
+ <SelectItem value="prod">Production</SelectItem>
18
+ <SelectItem value="staging">Staging</SelectItem>
19
+ <SelectItem value="dev">Dev</SelectItem>
20
+ </SelectContent>
21
+ </Select>
22
+ ),
23
+ // Opens the listbox (Radix portal), picks a different option, and confirms the
24
+ // trigger now reflects the new selection.
25
+ play: async ({ canvas, canvasElement, userEvent }) => {
26
+ const trigger = canvas.getByRole("combobox");
27
+ await expect(trigger).toHaveTextContent("Production");
28
+ await userEvent.click(trigger);
29
+ const body = within(canvasElement.ownerDocument.body);
30
+ await userEvent.click(await body.findByRole("option", { name: "Staging" }));
31
+ await expect(trigger).toHaveTextContent("Staging");
32
+ // A value that FITS gains no native `title` (#332): an unconditional one
33
+ // would silently become this combobox's accessible name — a field named
34
+ // after its own value is not a named field.
35
+ await expect(trigger).not.toHaveAttribute("title");
36
+ },
37
+ };
38
+
39
+ const LONG_LABEL = "prod-eu-west-1 · 2026-08-01 · 12 tools";
40
+
41
+ /**
42
+ * A long, composed value clipped by `line-clamp-1`, recovered on mouse hover
43
+ * by the automatic native `title` — the zero-extra-code path. The trigger
44
+ * measures its own content, so the `title` appears here and NOT on a short
45
+ * value (see `Default`, which has none).
46
+ *
47
+ * `aria-label` is on the trigger because the value is not a name: the `title`
48
+ * is a hover affordance, never the field's label.
49
+ */
50
+ export const ClippedValueGetsTitle: Story = {
51
+ render: () => (
52
+ <Select defaultValue={LONG_LABEL}>
53
+ <SelectTrigger className="w-56" aria-label="Environment">
54
+ <SelectValue />
55
+ </SelectTrigger>
56
+ <SelectContent>
57
+ <SelectItem value={LONG_LABEL}>{LONG_LABEL}</SelectItem>
58
+ <SelectItem value="dev">Dev</SelectItem>
59
+ </SelectContent>
60
+ </Select>
61
+ ),
62
+ play: async ({ canvas }) => {
63
+ const trigger = canvas.getByRole("combobox");
64
+ await waitFor(() => expect(trigger).toHaveAttribute("title", LONG_LABEL));
65
+ },
66
+ };
67
+
68
+ /**
69
+ * The keyboard-reachable recovery: a `Tooltip` composition, which Radix opens
70
+ * on focus as well as hover. `autoTitle={false}` is REQUIRED here — otherwise
71
+ * the native `title` and the Radix tooltip both appear on hover, one on top of
72
+ * the other.
73
+ */
74
+ export const LongComposedLabel: Story = {
75
+ render: () => (
76
+ <TooltipProvider delayDuration={0}>
77
+ <Select defaultValue={LONG_LABEL}>
78
+ <Tooltip>
79
+ <TooltipTrigger asChild>
80
+ <SelectTrigger className="w-56" aria-label="Environment" autoTitle={false}>
81
+ <SelectValue />
82
+ </SelectTrigger>
83
+ </TooltipTrigger>
84
+ <TooltipContent>{LONG_LABEL}</TooltipContent>
85
+ </Tooltip>
86
+ <SelectContent>
87
+ <SelectItem value={LONG_LABEL}>{LONG_LABEL}</SelectItem>
88
+ <SelectItem value="staging">Staging</SelectItem>
89
+ </SelectContent>
90
+ </Select>
91
+ </TooltipProvider>
92
+ ),
93
+ // Focus the trigger directly (the keyboard path, not a click) and confirm the
94
+ // tooltip content becomes visible — and that no competing native `title` was
95
+ // left behind.
96
+ play: async ({ canvas, canvasElement }) => {
97
+ const trigger = canvas.getByRole("combobox");
98
+ await expect(trigger).not.toHaveAttribute("title");
99
+
100
+ trigger.focus();
101
+ await expect(trigger).toHaveFocus();
102
+ const body = within(canvasElement.ownerDocument.body);
103
+ const content = await body.findByText(LONG_LABEL, { selector: "[data-side]" });
104
+ await waitFor(() => expect(content).toBeVisible());
105
+ },
106
+ };
@@ -0,0 +1,98 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { render, screen, waitFor } from "@testing-library/react";
3
+ import { Select, SelectTrigger, SelectValue } from "./select";
4
+
5
+ const LONG_LABEL = "prod-eu-west-1 · 2026-08-01 · 12 tools";
6
+
7
+ /**
8
+ * jsdom has no layout: every element reports `scrollWidth`/`clientWidth` 0, so
9
+ * nothing is ever "clipped" there. Fake the one measurement the component
10
+ * makes so the clipped branch is exercisable in a unit test; the real-layout
11
+ * behaviour is covered by the `ClippedValueGetsTitle` Storybook story.
12
+ */
13
+ function fakeClippedLayout() {
14
+ Object.defineProperty(HTMLElement.prototype, "scrollWidth", {
15
+ configurable: true,
16
+ get: () => 400,
17
+ });
18
+ Object.defineProperty(HTMLElement.prototype, "clientWidth", {
19
+ configurable: true,
20
+ get: () => 100,
21
+ });
22
+ }
23
+
24
+ afterEach(() => {
25
+ // Restore jsdom's own getters (they live on the Element.prototype below).
26
+ Reflect.deleteProperty(HTMLElement.prototype, "scrollWidth");
27
+ Reflect.deleteProperty(HTMLElement.prototype, "clientWidth");
28
+ });
29
+
30
+ describe("SelectTrigger", () => {
31
+ it("sets no title while the rendered text fits (an unconditional title names the field after its own value)", async () => {
32
+ render(
33
+ <Select>
34
+ <SelectTrigger aria-label="Environment">
35
+ <SelectValue placeholder={LONG_LABEL} />
36
+ </SelectTrigger>
37
+ </Select>,
38
+ );
39
+ const trigger = screen.getByRole("combobox");
40
+ // Let the mount effect and its observers run before asserting the absence.
41
+ await waitFor(() => expect(trigger).toHaveAttribute("data-size", "default"));
42
+ expect(trigger).not.toHaveAttribute("title");
43
+ });
44
+
45
+ it("auto-sets title from the rendered text once that text is clipped", async () => {
46
+ fakeClippedLayout();
47
+ render(
48
+ <Select>
49
+ <SelectTrigger aria-label="Environment">
50
+ <SelectValue placeholder={LONG_LABEL} />
51
+ </SelectTrigger>
52
+ </Select>,
53
+ );
54
+ await waitFor(() => expect(screen.getByRole("combobox")).toHaveAttribute("title", LONG_LABEL));
55
+ });
56
+
57
+ it("separates composed trigger content instead of concatenating it", async () => {
58
+ fakeClippedLayout();
59
+ render(
60
+ <Select>
61
+ <SelectTrigger aria-label="Environment">
62
+ <span>Env:</span>
63
+ <SelectValue placeholder="Staging" />
64
+ </SelectTrigger>
65
+ </Select>,
66
+ );
67
+ await waitFor(() =>
68
+ expect(screen.getByRole("combobox")).toHaveAttribute("title", "Env: Staging"),
69
+ );
70
+ });
71
+
72
+ it("never overrides a caller-supplied title", async () => {
73
+ fakeClippedLayout();
74
+ render(
75
+ <Select>
76
+ <SelectTrigger title="Custom title" aria-label="Environment">
77
+ <SelectValue placeholder="Environment" />
78
+ </SelectTrigger>
79
+ </Select>,
80
+ );
81
+ const trigger = screen.getByRole("combobox");
82
+ await waitFor(() => expect(trigger).toHaveAttribute("title", "Custom title"));
83
+ });
84
+
85
+ it("sets no title when autoTitle is disabled, even while clipped", async () => {
86
+ fakeClippedLayout();
87
+ render(
88
+ <Select>
89
+ <SelectTrigger autoTitle={false} aria-label="Environment">
90
+ <SelectValue placeholder={LONG_LABEL} />
91
+ </SelectTrigger>
92
+ </Select>,
93
+ );
94
+ const trigger = screen.getByRole("combobox");
95
+ await waitFor(() => expect(trigger).toHaveAttribute("data-size", "default"));
96
+ expect(trigger).not.toHaveAttribute("title");
97
+ });
98
+ });
@@ -0,0 +1,230 @@
1
+ import {
2
+ forwardRef,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ type ComponentPropsWithoutRef,
7
+ type ElementRef,
8
+ } from "react";
9
+ import * as SelectPrimitive from "@radix-ui/react-select";
10
+ import { Check, ChevronDown, ChevronUp } from "lucide-react";
11
+ import { cn } from "../../lib/cn";
12
+ import { mergeRefs } from "../../lib/merge-refs";
13
+
14
+ export const Select = SelectPrimitive.Root;
15
+ export const SelectGroup = SelectPrimitive.Group;
16
+ export const SelectValue = SelectPrimitive.Value;
17
+
18
+ /** Sub-pixel layout rounding — an element is only "clipped" beyond this. */
19
+ const CLIP_SLACK_PX = 1;
20
+
21
+ function isClipped(el: Element) {
22
+ return (
23
+ el.scrollWidth > el.clientWidth + CLIP_SLACK_PX ||
24
+ el.scrollHeight > el.clientHeight + CLIP_SLACK_PX
25
+ );
26
+ }
27
+
28
+ /**
29
+ * The trigger's visible text, ONE SEGMENT PER CHILD NODE joined by a space, so
30
+ * a prefix/badge beside `SelectValue` reads `"Env: Staging"` — a bare
31
+ * `textContent` read concatenates the DOM's text nodes with no separator and
32
+ * produces `"Env:Staging"`.
33
+ */
34
+ function composeTriggerText(node: HTMLElement) {
35
+ return Array.from(node.childNodes)
36
+ .map((child) => child.textContent?.replace(/\s+/gu, " ").trim() ?? "")
37
+ .filter(Boolean)
38
+ .join(" ");
39
+ }
40
+
41
+ export type SelectTriggerProps = ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
42
+ size?: "sm" | "default";
43
+ /**
44
+ * Auto-set a native `title` from the trigger's rendered text — but ONLY
45
+ * while that text is actually clipped (default `true`).
46
+ *
47
+ * Set `false` when the trigger is a `TooltipTrigger`: the native tooltip and
48
+ * the Radix one would otherwise both appear on hover. A caller-supplied
49
+ * `title` also disables it (an explicit `title` is never overridden).
50
+ */
51
+ autoTitle?: boolean;
52
+ };
53
+
54
+ /**
55
+ * `[&>span]:line-clamp-1` clips the rendered `SelectValue` — necessary so a
56
+ * long/composed label doesn't blow out the trigger's width. To keep clipped
57
+ * text recoverable, the trigger MEASURES its content and sets a native `title`
58
+ * only while something is genuinely cut off, so it's readable on mouse hover
59
+ * with zero extra code.
60
+ *
61
+ * The measurement is not cosmetic: an unconditional `title` becomes the
62
+ * button's accessible name, which would paper over a real "this combobox has
63
+ * no name" a11y failure with the field's own VALUE — a value is not a name.
64
+ * Label the trigger properly (`aria-label`/`aria-labelledby`) regardless.
65
+ *
66
+ * `title` also never reaches keyboard users — for that, wrap the trigger in
67
+ * `Tooltip`/`TooltipTrigger asChild`/`TooltipContent` and pass
68
+ * `autoTitle={false}` (see the `LongComposedLabel` story), which Radix opens
69
+ * on keyboard focus too.
70
+ */
71
+ export const SelectTrigger = forwardRef<
72
+ ElementRef<typeof SelectPrimitive.Trigger>,
73
+ SelectTriggerProps
74
+ >(function SelectTrigger(
75
+ { className, children, size = "default", title, autoTitle = true, ...props },
76
+ ref,
77
+ ) {
78
+ const innerRef = useRef<HTMLButtonElement>(null);
79
+ const mergedRef = useMemo(() => mergeRefs(ref, innerRef), [ref]);
80
+
81
+ useEffect(() => {
82
+ if (!autoTitle) return;
83
+ if (title !== undefined) return; // caller owns `title` — never override it
84
+ const node = innerRef.current;
85
+ if (!node) return;
86
+
87
+ const sync = () => {
88
+ // The clamp lives on the child spans, but a long unbroken value can also
89
+ // overflow the trigger box itself — check both.
90
+ const clipped = isClipped(node) || Array.from(node.children).some(isClipped);
91
+ const text = clipped ? composeTriggerText(node) : "";
92
+ if (text) node.title = text;
93
+ else node.removeAttribute("title");
94
+ };
95
+ sync();
96
+
97
+ // The selected item's text isn't a React child of this component — Radix
98
+ // portals it into the (child) `SelectValue` node from `SelectContent`
99
+ // asynchronously, on its OWN commit, so a one-shot read here can run
100
+ // before that text lands. Observe the subtree instead of trusting this
101
+ // component's own render cycle. (Attributes are deliberately NOT observed:
102
+ // `sync` writes `title`, which would otherwise re-enter.)
103
+ const observer = new MutationObserver(sync);
104
+ observer.observe(node, { childList: true, subtree: true, characterData: true });
105
+
106
+ // Clipping is a function of WIDTH, so it has to be re-measured when the
107
+ // trigger resizes, not only when its text changes.
108
+ const resizeObserver =
109
+ typeof ResizeObserver === "function" ? new ResizeObserver(sync) : undefined;
110
+ resizeObserver?.observe(node);
111
+
112
+ return () => {
113
+ observer.disconnect();
114
+ resizeObserver?.disconnect();
115
+ };
116
+ }, [autoTitle, title]);
117
+
118
+ return (
119
+ <SelectPrimitive.Trigger
120
+ ref={mergedRef}
121
+ data-size={size}
122
+ title={title}
123
+ className={cn(
124
+ "flex w-full items-center justify-between gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm",
125
+ size === "sm" ? "h-8" : "h-9",
126
+ "placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1 focus:ring-offset-background",
127
+ // No `disabled:cursor-*` override — disabled controls keep the arrow
128
+ // automatically (interaction-guidelines.md), matching Combobox's
129
+ // Button-based trigger (`disabled:pointer-events-none`) (#343).
130
+ "disabled:opacity-50 disabled:border-border [&>span]:line-clamp-1",
131
+ className,
132
+ )}
133
+ {...props}
134
+ >
135
+ {children}
136
+ <SelectPrimitive.Icon asChild>
137
+ <ChevronDown className="size-4 opacity-50" />
138
+ </SelectPrimitive.Icon>
139
+ </SelectPrimitive.Trigger>
140
+ );
141
+ });
142
+
143
+ export const SelectContent = forwardRef<
144
+ ElementRef<typeof SelectPrimitive.Content>,
145
+ ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
146
+ >(function SelectContent({ className, children, position = "popper", ...props }, ref) {
147
+ return (
148
+ <SelectPrimitive.Portal>
149
+ <SelectPrimitive.Content
150
+ ref={ref}
151
+ position={position}
152
+ className={cn(
153
+ "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md bg-popover text-popover-foreground shadow-ring-md",
154
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95",
155
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
156
+ position === "popper" &&
157
+ "data-[side=bottom]:translate-y-1 data-[side=top]:-translate-y-1",
158
+ className,
159
+ )}
160
+ {...props}
161
+ >
162
+ <SelectPrimitive.ScrollUpButton className="flex cursor-default items-center justify-center py-1">
163
+ <ChevronUp className="size-4" />
164
+ </SelectPrimitive.ScrollUpButton>
165
+ <SelectPrimitive.Viewport
166
+ className={cn(
167
+ "p-1",
168
+ position === "popper" &&
169
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
170
+ )}
171
+ >
172
+ {children}
173
+ </SelectPrimitive.Viewport>
174
+ <SelectPrimitive.ScrollDownButton className="flex cursor-default items-center justify-center py-1">
175
+ <ChevronDown className="size-4" />
176
+ </SelectPrimitive.ScrollDownButton>
177
+ </SelectPrimitive.Content>
178
+ </SelectPrimitive.Portal>
179
+ );
180
+ });
181
+
182
+ export const SelectLabel = forwardRef<
183
+ ElementRef<typeof SelectPrimitive.Label>,
184
+ ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
185
+ >(function SelectLabel({ className, ...props }, ref) {
186
+ return (
187
+ <SelectPrimitive.Label
188
+ ref={ref}
189
+ className={cn("px-2 py-1.5 text-xs font-semibold text-muted-foreground", className)}
190
+ {...props}
191
+ />
192
+ );
193
+ });
194
+
195
+ export const SelectItem = forwardRef<
196
+ ElementRef<typeof SelectPrimitive.Item>,
197
+ ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
198
+ >(function SelectItem({ className, children, ...props }, ref) {
199
+ return (
200
+ <SelectPrimitive.Item
201
+ ref={ref}
202
+ className={cn(
203
+ "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 ps-2 pe-8 text-sm outline-none transition-colors duration-fast",
204
+ "focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
205
+ className,
206
+ )}
207
+ {...props}
208
+ >
209
+ <span className="absolute end-2 flex size-3.5 items-center justify-center">
210
+ <SelectPrimitive.ItemIndicator>
211
+ <Check className="size-4" />
212
+ </SelectPrimitive.ItemIndicator>
213
+ </span>
214
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
215
+ </SelectPrimitive.Item>
216
+ );
217
+ });
218
+
219
+ export const SelectSeparator = forwardRef<
220
+ ElementRef<typeof SelectPrimitive.Separator>,
221
+ ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
222
+ >(function SelectSeparator({ className, ...props }, ref) {
223
+ return (
224
+ <SelectPrimitive.Separator
225
+ ref={ref}
226
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
227
+ {...props}
228
+ />
229
+ );
230
+ });
@@ -0,0 +1,2 @@
1
+ export { Separator } from "./separator";
2
+ export type { SeparatorProps } from "./separator";
@@ -0,0 +1,87 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Separator } from "./separator";
3
+ const meta = {
4
+ title: "Display/Separator",
5
+ component: Separator,
6
+ tags: ["autodocs"],
7
+ argTypes: {
8
+ orientation: {
9
+ description: "Direction of the line.",
10
+ control: { type: "radio" },
11
+ options: ["horizontal", "vertical"],
12
+ table: { category: "Appearance" },
13
+ },
14
+ tone: {
15
+ description:
16
+ "`subtle` uses `--border` (decorative/redundant); `strong` uses `--border-strong` (≥3:1, sole structural cue).",
17
+ control: { type: "radio" },
18
+ options: ["subtle", "strong"],
19
+ table: { category: "Appearance" },
20
+ },
21
+ decorative: {
22
+ description: "When true the separator is hidden from the accessibility tree (role=none).",
23
+ control: "boolean",
24
+ table: { category: "Behavior" },
25
+ },
26
+ className: {
27
+ description: "Extra Tailwind classes merged via cn().",
28
+ control: "text",
29
+ table: { category: "Appearance" },
30
+ },
31
+ },
32
+ } satisfies Meta<typeof Separator>;
33
+ export default meta;
34
+ type Story = StoryObj<typeof meta>;
35
+
36
+ /**
37
+ * Default (subtle) — uses `--border`. The boundary is a decorative/redundant
38
+ * cue (surrounding fill, elevation, or spacing already signals the edge).
39
+ * WCAG 1.4.11 redundant-boundary exemption applies.
40
+ */
41
+ export const Default: Story = {
42
+ render: () => (
43
+ <div className="w-64 text-body">
44
+ <p>Above</p>
45
+ <Separator className="my-3" />
46
+ <p>Below</p>
47
+ </div>
48
+ ),
49
+ };
50
+
51
+ /**
52
+ * Strong — uses `--border-strong` (≥3:1 vs `--card`/`--background`). Use when
53
+ * the separator is the ONLY structural cue between two same-surface regions.
54
+ * Decision test: "If I deleted this line, could a sighted user still tell the
55
+ * two regions apart?" No → `tone="strong"`.
56
+ */
57
+ export const Strong: Story = {
58
+ render: () => (
59
+ <div className="w-64 rounded-lg bg-card p-4 text-body">
60
+ <p className="font-medium">Section A</p>
61
+ <Separator tone="strong" className="my-3" />
62
+ <p className="font-medium">Section B</p>
63
+ </div>
64
+ ),
65
+ };
66
+
67
+ /** Vertical orientation, default (subtle) tone. */
68
+ export const Vertical: Story = {
69
+ render: () => (
70
+ <div className="flex h-8 items-center gap-4 text-body">
71
+ <span>Left</span>
72
+ <Separator orientation="vertical" />
73
+ <span>Right</span>
74
+ </div>
75
+ ),
76
+ };
77
+
78
+ /** Vertical orientation, strong tone — the only structural cue between columns. */
79
+ export const VerticalStrong: Story = {
80
+ render: () => (
81
+ <div className="flex h-8 items-center gap-4 text-body">
82
+ <span>Left</span>
83
+ <Separator orientation="vertical" tone="strong" />
84
+ <span>Right</span>
85
+ </div>
86
+ ),
87
+ };
@@ -0,0 +1,51 @@
1
+ import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
2
+ import * as SeparatorPrimitive from "@radix-ui/react-separator";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { cn } from "../../lib/cn";
5
+
6
+ const separatorVariants = cva("shrink-0", {
7
+ variants: {
8
+ /**
9
+ * `subtle` (default) — uses `--border` (a decorative/redundant boundary;
10
+ * compliant because the edge is also signalled by surrounding fill, elevation
11
+ * or spacing — WCAG 1.4.11 redundant-boundary exemption).
12
+ *
13
+ * `strong` — uses `--border-strong` (≥3:1 vs `--card`/`--background`).
14
+ * Use when the separator is the ONLY structural cue between two same-surface
15
+ * regions (no fill, elevation or spacing change to fall back on).
16
+ * Decision test: "If I deleted this line, could a sighted user still tell
17
+ * the two regions apart?" No → `strong`.
18
+ */
19
+ tone: {
20
+ subtle: "bg-border",
21
+ strong: "bg-border-strong",
22
+ },
23
+ },
24
+ defaultVariants: {
25
+ tone: "subtle",
26
+ },
27
+ });
28
+
29
+ export type SeparatorProps = ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> &
30
+ VariantProps<typeof separatorVariants>;
31
+
32
+ export const Separator = forwardRef<ElementRef<typeof SeparatorPrimitive.Root>, SeparatorProps>(
33
+ function Separator(
34
+ { className, orientation = "horizontal", decorative = true, tone, ...props },
35
+ ref,
36
+ ) {
37
+ return (
38
+ <SeparatorPrimitive.Root
39
+ ref={ref}
40
+ decorative={decorative}
41
+ orientation={orientation}
42
+ className={cn(
43
+ separatorVariants({ tone }),
44
+ orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
45
+ className,
46
+ )}
47
+ {...props}
48
+ />
49
+ );
50
+ },
51
+ );
@@ -0,0 +1,12 @@
1
+ export {
2
+ Sheet,
3
+ SheetTrigger,
4
+ SheetClose,
5
+ SheetPortal,
6
+ SheetContent,
7
+ SheetHeader,
8
+ SheetFooter,
9
+ SheetTitle,
10
+ SheetDescription,
11
+ type SheetContentProps,
12
+ } from "./sheet";
@@ -0,0 +1,65 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, waitFor, within } from "storybook/test";
3
+ import { Button } from "../button";
4
+ import {
5
+ Sheet,
6
+ SheetContent,
7
+ SheetDescription,
8
+ SheetHeader,
9
+ SheetTitle,
10
+ SheetTrigger,
11
+ } from "./sheet";
12
+ const meta = {
13
+ title: "Overlays/Sheet",
14
+ component: Sheet,
15
+ tags: ["autodocs"],
16
+ argTypes: {
17
+ open: {
18
+ description: "Controlled open state of the sheet.",
19
+ control: "boolean",
20
+ table: { category: "State" },
21
+ },
22
+ defaultOpen: {
23
+ description: "Uncontrolled initial open state.",
24
+ control: "boolean",
25
+ table: { category: "State" },
26
+ },
27
+ onOpenChange: {
28
+ description: "Callback when the open state changes.",
29
+ control: false,
30
+ table: { category: "Events" },
31
+ },
32
+ modal: {
33
+ description: "Whether the sheet is modal (disables outside interaction). Default true.",
34
+ control: "boolean",
35
+ table: { category: "Behaviour" },
36
+ },
37
+ },
38
+ } satisfies Meta<typeof Sheet>;
39
+ export default meta;
40
+ type Story = StoryObj<typeof meta>;
41
+ export const Default: Story = {
42
+ render: () => (
43
+ <Sheet>
44
+ <SheetTrigger asChild>
45
+ <Button variant="outline">Open sheet</Button>
46
+ </SheetTrigger>
47
+ <SheetContent>
48
+ <SheetHeader>
49
+ <SheetTitle>Settings</SheetTitle>
50
+ <SheetDescription>Manage your workspace.</SheetDescription>
51
+ </SheetHeader>
52
+ </SheetContent>
53
+ </Sheet>
54
+ ),
55
+ // Opens the sheet (portalled to body), confirms the title and description
56
+ // are visible, then presses Escape to dismiss it.
57
+ play: async ({ canvas, canvasElement, userEvent }) => {
58
+ await userEvent.click(canvas.getByRole("button", { name: /open sheet/i }));
59
+ const body = within(canvasElement.ownerDocument.body);
60
+ const sheet = await body.findByRole("dialog");
61
+ await waitFor(() => expect(within(sheet).getByText("Settings")).toBeVisible());
62
+ await waitFor(() => expect(within(sheet).getByText("Manage your workspace.")).toBeVisible());
63
+ await userEvent.keyboard("{Escape}");
64
+ },
65
+ };