@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,57 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, waitFor, within } from "storybook/test";
3
+ import { Button } from "../button";
4
+ import { HoverCard, HoverCardContent, HoverCardTrigger } from "./hover-card";
5
+ const meta = {
6
+ title: "Overlays/HoverCard",
7
+ component: HoverCard,
8
+ tags: ["autodocs"],
9
+ argTypes: {
10
+ open: {
11
+ description: "Controlled open state of the hover card.",
12
+ control: "boolean",
13
+ table: { category: "State" },
14
+ },
15
+ defaultOpen: {
16
+ description: "Uncontrolled initial open state.",
17
+ control: "boolean",
18
+ table: { category: "State" },
19
+ },
20
+ onOpenChange: {
21
+ description: "Callback when the open state changes.",
22
+ control: false,
23
+ table: { category: "Events" },
24
+ },
25
+ openDelay: {
26
+ description: "Milliseconds to wait before opening on pointer enter. Default 700.",
27
+ control: "number",
28
+ table: { category: "Behaviour" },
29
+ },
30
+ closeDelay: {
31
+ description: "Milliseconds to wait before closing on pointer leave. Default 300.",
32
+ control: "number",
33
+ table: { category: "Behaviour" },
34
+ },
35
+ },
36
+ } satisfies Meta<typeof HoverCard>;
37
+ export default meta;
38
+ type Story = StoryObj<typeof meta>;
39
+ export const Default: Story = {
40
+ render: () => (
41
+ <HoverCard openDelay={0} closeDelay={0}>
42
+ <HoverCardTrigger asChild>
43
+ <Button variant="link">@brand</Button>
44
+ </HoverCardTrigger>
45
+ <HoverCardContent>The brand-ui design system.</HoverCardContent>
46
+ </HoverCard>
47
+ ),
48
+ // Hovers over the trigger (delay zeroed for determinism) to open the portalled
49
+ // hover card and confirms the content is visible.
50
+ play: async ({ canvas, canvasElement, userEvent }) => {
51
+ const trigger = canvas.getByRole("button", { name: "@brand" });
52
+ await userEvent.hover(trigger);
53
+ const body = within(canvasElement.ownerDocument.body);
54
+ const card = await body.findByText("The brand-ui design system.");
55
+ await waitFor(() => expect(card).toBeVisible());
56
+ },
57
+ };
@@ -0,0 +1,25 @@
1
+ import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
2
+ import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
3
+ import { cn } from "../../lib/cn";
4
+
5
+ export const HoverCard = HoverCardPrimitive.Root;
6
+ export const HoverCardTrigger = HoverCardPrimitive.Trigger;
7
+
8
+ export const HoverCardContent = forwardRef<
9
+ ElementRef<typeof HoverCardPrimitive.Content>,
10
+ ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
11
+ >(function HoverCardContent({ className, align = "center", sideOffset = 4, ...props }, ref) {
12
+ return (
13
+ <HoverCardPrimitive.Content
14
+ ref={ref}
15
+ align={align}
16
+ sideOffset={sideOffset}
17
+ className={cn(
18
+ "z-50 w-64 rounded-md bg-popover p-4 text-popover-foreground shadow-ring-md outline-none",
19
+ "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",
20
+ className,
21
+ )}
22
+ {...props}
23
+ />
24
+ );
25
+ });
@@ -0,0 +1 @@
1
+ export { HoverCard, HoverCardTrigger, HoverCardContent } from "./hover-card";
@@ -0,0 +1,108 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, waitFor, within } from "storybook/test";
3
+ import { RefreshCw, Trash2 } from "lucide-react";
4
+ import { IconButton } from "./icon-button";
5
+
6
+ const meta = {
7
+ title: "Core/IconButton",
8
+ component: IconButton,
9
+ tags: ["autodocs"],
10
+ args: {
11
+ label: "Refresh",
12
+ icon: <RefreshCw aria-hidden="true" />,
13
+ },
14
+ argTypes: {
15
+ label: {
16
+ description: "Accessible name AND tooltip text — a single source of truth.",
17
+ control: "text",
18
+ table: { category: "Content" },
19
+ },
20
+ icon: { control: false, table: { category: "Content" } },
21
+ variant: {
22
+ control: "select",
23
+ options: [
24
+ "default",
25
+ "secondary",
26
+ "destructive",
27
+ "outline",
28
+ "outline-subtle",
29
+ "ghost",
30
+ "link",
31
+ ],
32
+ table: { category: "Appearance" },
33
+ },
34
+ size: {
35
+ control: "select",
36
+ options: ["icon", "icon-sm", "icon-lg"],
37
+ table: { category: "Appearance" },
38
+ },
39
+ disabled: { control: "boolean", table: { category: "State" } },
40
+ disabledReason: {
41
+ description:
42
+ "Shown appended to the tooltip and exposed via aria-describedby, even while disabled.",
43
+ control: "text",
44
+ table: { category: "State" },
45
+ },
46
+ side: {
47
+ control: "select",
48
+ options: ["top", "right", "bottom", "left"],
49
+ table: { category: "Appearance" },
50
+ },
51
+ },
52
+ } satisfies Meta<typeof IconButton>;
53
+ export default meta;
54
+ type Story = StoryObj<typeof meta>;
55
+
56
+ export const Default: Story = {
57
+ // Opens on keyboard focus (no pointer) — the a11y-critical path this
58
+ // component exists for (#335). The tooltip content is Radix-portalled to
59
+ // `document.body`, outside the story's own canvas root — query the owner
60
+ // document's body, not `canvas` (mirrors Overlays/Tooltip's Default story).
61
+ play: async ({ canvas, canvasElement, userEvent }) => {
62
+ const button = canvas.getByRole("button", { name: "Refresh" });
63
+ await expect(button).not.toHaveAttribute("title");
64
+ await userEvent.tab();
65
+ await expect(button).toHaveFocus();
66
+ const body = within(canvasElement.ownerDocument.body);
67
+ const tooltip = await body.findByText("Refresh", { selector: "[data-side]" });
68
+ await waitFor(() => expect(tooltip).toBeVisible());
69
+ },
70
+ };
71
+
72
+ export const Disabled: Story = {
73
+ args: { disabled: true },
74
+ play: async ({ canvas }) => {
75
+ const button = canvas.getByRole("button", { name: "Refresh" });
76
+ await expect(button).toBeDisabled();
77
+ },
78
+ };
79
+
80
+ /** A disabled control that still explains WHY via the tooltip + `aria-describedby`. */
81
+ export const DisabledWithReason: Story = {
82
+ name: "Disabled (with reason)",
83
+ args: {
84
+ label: "Delete",
85
+ icon: <Trash2 aria-hidden="true" />,
86
+ variant: "destructive",
87
+ disabled: true,
88
+ disabledReason: "You don't have permission to delete this item.",
89
+ },
90
+ play: async ({ canvas }) => {
91
+ const button = canvas.getByRole("button", { name: "Delete" });
92
+ await expect(button).toBeDisabled();
93
+ const describedBy = button.getAttribute("aria-describedby");
94
+ await expect(describedBy).toBeTruthy();
95
+ const description = canvas.getByText("You don't have permission to delete this item.");
96
+ await expect(description.id).toBe(describedBy);
97
+ },
98
+ };
99
+
100
+ export const Sizes: Story = {
101
+ render: (args) => (
102
+ <div className="flex items-center gap-2">
103
+ <IconButton {...args} size="icon-sm" label="Small" />
104
+ <IconButton {...args} size="icon" label="Default" />
105
+ <IconButton {...args} size="icon-lg" label="Large" />
106
+ </div>
107
+ ),
108
+ };
@@ -0,0 +1,93 @@
1
+ import { createRef } from "react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { render, screen, waitFor } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+ import { RefreshCw } from "lucide-react";
6
+ import { IconButton } from "./icon-button";
7
+
8
+ describe("IconButton", () => {
9
+ it("has exactly one accessible name and no title attribute anywhere in the output", () => {
10
+ render(<IconButton label="Refresh" icon={<RefreshCw aria-hidden="true" />} />);
11
+ const button = screen.getByRole("button", { name: "Refresh" });
12
+ expect(button).not.toHaveAttribute("title");
13
+ expect(document.querySelector("[title]")).toBeNull();
14
+ });
15
+
16
+ it("opens the tooltip on keyboard focus alone (no pointer interaction)", async () => {
17
+ render(<IconButton label="Refresh" icon={<RefreshCw aria-hidden="true" />} />);
18
+ await userEvent.tab();
19
+ expect(screen.getByRole("button", { name: "Refresh" })).toHaveFocus();
20
+ const tooltip = await screen.findByText("Refresh", { selector: "[data-side]" });
21
+ await waitFor(() => expect(tooltip).toBeVisible());
22
+ });
23
+
24
+ it("exposes disabledReason via aria-describedby on an always-present element, even while pointer-events-none", () => {
25
+ render(
26
+ <IconButton
27
+ label="Delete"
28
+ icon={<RefreshCw aria-hidden="true" />}
29
+ disabled
30
+ disabledReason="You don't have permission"
31
+ />,
32
+ );
33
+ const button = screen.getByRole("button", { name: "Delete" });
34
+ expect(button).toBeDisabled();
35
+ const describedBy = button.getAttribute("aria-describedby");
36
+ expect(describedBy).toBeTruthy();
37
+ const node = describedBy ? document.getElementById(describedBy) : null;
38
+ expect(node).not.toBeNull();
39
+ expect(node).toHaveTextContent("You don't have permission");
40
+ });
41
+
42
+ it("does not render a description node (or aria-describedby) when no disabledReason is given", () => {
43
+ render(<IconButton label="Refresh" icon={<RefreshCw aria-hidden="true" />} />);
44
+ const button = screen.getByRole("button", { name: "Refresh" });
45
+ expect(button).not.toHaveAttribute("aria-describedby");
46
+ });
47
+
48
+ // While disabled, the wrapper span — not the button — is the tab stop, so it
49
+ // must carry the tokened focus ring; without it the browser's own hardcoded
50
+ // blue outline shows through, identically in every theme.
51
+ it("gives the disabled-state tab stop the tokened focus ring", async () => {
52
+ render(
53
+ <IconButton
54
+ label="Delete"
55
+ icon={<RefreshCw aria-hidden="true" />}
56
+ disabled
57
+ disabledReason="No permission"
58
+ />,
59
+ );
60
+ const wrapper = document.querySelector('[data-slot="icon-button"]')!;
61
+ expect(wrapper).toHaveAttribute("tabindex", "0");
62
+ expect(wrapper.className).toContain("focus-visible:ring-ring");
63
+ await userEvent.tab();
64
+ expect(wrapper).toHaveFocus();
65
+ });
66
+
67
+ it("does not put the wrapper in the tab order when enabled (no phantom stop)", async () => {
68
+ render(<IconButton label="Refresh" icon={<RefreshCw aria-hidden="true" />} />);
69
+ const wrapper = document.querySelector('[data-slot="icon-button"]')!;
70
+ expect(wrapper).not.toHaveAttribute("tabindex");
71
+ await userEvent.tab();
72
+ expect(screen.getByRole("button", { name: "Refresh" })).toHaveFocus();
73
+ });
74
+
75
+ it("forwards the ref to the underlying <button>", () => {
76
+ const ref = createRef<HTMLButtonElement>();
77
+ render(<IconButton ref={ref} label="Refresh" icon={<RefreshCw aria-hidden="true" />} />);
78
+ expect(ref.current).toBeInstanceOf(HTMLButtonElement);
79
+ expect(ref.current).toBe(screen.getByRole("button", { name: "Refresh" }));
80
+ });
81
+
82
+ it("cannot have its computed aria-label overridden by a stray prop (single source of truth)", () => {
83
+ // TypeScript's JSX checker special-cases aria-*/data-* attribute names on
84
+ // ANY element regardless of the declared prop type, so `aria-label` is
85
+ // syntactically accepted here even though `IconButtonProps` omits it —
86
+ // this test locks the RUNTIME guarantee that it can never win.
87
+ render(
88
+ <IconButton label="Refresh" icon={<RefreshCw aria-hidden="true" />} aria-label="Wrong" />,
89
+ );
90
+ expect(screen.getByRole("button", { name: "Refresh" })).toBeInTheDocument();
91
+ expect(screen.queryByRole("button", { name: "Wrong" })).not.toBeInTheDocument();
92
+ });
93
+ });
@@ -0,0 +1,139 @@
1
+ import {
2
+ forwardRef,
3
+ useId,
4
+ type ButtonHTMLAttributes,
5
+ type ComponentProps,
6
+ type ReactNode,
7
+ } from "react";
8
+ import type { VariantProps } from "class-variance-authority";
9
+ import { cn } from "../../lib/cn";
10
+ import { Button } from "../button";
11
+ import type { buttonVariants } from "../button";
12
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../tooltip";
13
+
14
+ /** The icon-only size axis (a deliberate subset of `buttonVariants`' `size`). */
15
+ export type IconButtonSize = "icon" | "icon-sm" | "icon-lg";
16
+
17
+ export interface IconButtonProps
18
+ extends
19
+ Omit<
20
+ ButtonHTMLAttributes<HTMLButtonElement>,
21
+ "title" | "children" | "aria-label" | "aria-describedby"
22
+ >,
23
+ Pick<VariantProps<typeof buttonVariants>, "variant"> {
24
+ /**
25
+ * Accessible name AND tooltip text — a single source of truth, so the two
26
+ * can never drift out of sync (the failure this component exists to close:
27
+ * `aria-label` and a tooltip's children set independently, most call sites
28
+ * only setting one).
29
+ */
30
+ label: string;
31
+ /** The glyph (typically a `lucide-react` element). */
32
+ icon: ReactNode;
33
+ /** Icon-only size axis. Default `"icon"`. */
34
+ size?: IconButtonSize;
35
+ /**
36
+ * Shown appended to the tooltip text, and — independently — exposed via
37
+ * `aria-describedby` through an `sr-only` node that is present whenever this
38
+ * prop is (i.e. it does NOT depend on the tooltip being open, and it is not
39
+ * rendered at all when there is no reason — so it can never be read as stray
40
+ * text), so assistive tech gets the reason even while the button is disabled
41
+ * (a disabled
42
+ * `<button>` still exposes its ARIA attributes; it just isn't
43
+ * focusable/hoverable itself, which is why the tooltip trigger wraps a
44
+ * focusable `<span>` instead — see the render below).
45
+ */
46
+ disabledReason?: string;
47
+ /** Tooltip placement. Default `"top"`. */
48
+ side?: ComponentProps<typeof TooltipContent>["side"];
49
+ }
50
+
51
+ /**
52
+ * A single affordance for an icon-only control: `label` becomes both the
53
+ * `aria-label` and the tooltip text, so they cannot diverge. Composes
54
+ * `Button` + `Tooltip` — it does not reimplement either.
55
+ *
56
+ * Deliberately has **no `asChild`**: accepting one would dissolve the button
57
+ * into a Radix `Slot` and break the tooltip-trigger wrapper below. A
58
+ * link-styled icon button should compose `Button asChild` + `Tooltip`
59
+ * directly instead.
60
+ */
61
+ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(function IconButton(
62
+ {
63
+ label,
64
+ icon,
65
+ variant = "ghost",
66
+ size = "icon",
67
+ disabled,
68
+ disabledReason,
69
+ side = "top",
70
+ className,
71
+ ...props
72
+ },
73
+ ref,
74
+ ) {
75
+ const reasonId = useId();
76
+ // The visible tooltip text always LEADS with `label` verbatim (never only
77
+ // the reason), so it can never contradict the accessible name (WCAG 2.5.3
78
+ // Label in Name) — the reason is appended context, not a replacement name.
79
+ const tooltipText = disabledReason ? `${label} — ${disabledReason}` : label;
80
+
81
+ return (
82
+ // Self-provided so this works anywhere — a consumer shouldn't have to
83
+ // remember to mount a global TooltipProvider for one icon button.
84
+ <TooltipProvider>
85
+ <Tooltip>
86
+ <TooltipTrigger asChild>
87
+ {/*
88
+ * A disabled `Button` carries `pointer-events-none`, so hover never
89
+ * reaches it and the tooltip would never open. Wrap the trigger
90
+ * around a plain <span> instead of the <button> itself: it stays
91
+ * hoverable always, and gains `tabIndex={0}` only while the inner
92
+ * control is disabled (and therefore removed from the tab order),
93
+ * so keyboard users can still reach the tooltip without a doubled
94
+ * stop when the button is enabled.
95
+ */}
96
+ <span
97
+ data-slot="icon-button"
98
+ tabIndex={disabled ? 0 : undefined}
99
+ className={cn(
100
+ "inline-flex rounded-md",
101
+ // While disabled this span IS the focus target (the button is out
102
+ // of the tab order), so it must carry the system's focus ring —
103
+ // without this it falls back to the UA default outline, which is
104
+ // a hardcoded browser blue identical in all three themes.
105
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
106
+ disabled && "cursor-not-allowed",
107
+ )}
108
+ >
109
+ <Button
110
+ ref={ref}
111
+ variant={variant}
112
+ size={size}
113
+ disabled={disabled}
114
+ {...props}
115
+ // Computed AFTER the spread so a stray `aria-label`/`aria-describedby`
116
+ // in `...props` can never win over the derived, single-source-of-truth
117
+ // values (the type above already excludes both, this is defense in
118
+ // depth for untyped/JS callers).
119
+ aria-label={label}
120
+ aria-describedby={disabledReason ? reasonId : undefined}
121
+ data-slot="icon-button-control"
122
+ className={className}
123
+ >
124
+ {icon}
125
+ </Button>
126
+ </span>
127
+ </TooltipTrigger>
128
+ <TooltipContent side={side} data-slot="icon-button-tooltip">
129
+ {tooltipText}
130
+ </TooltipContent>
131
+ </Tooltip>
132
+ {disabledReason ? (
133
+ <span id={reasonId} data-slot="icon-button-description" className="sr-only">
134
+ {disabledReason}
135
+ </span>
136
+ ) : null}
137
+ </TooltipProvider>
138
+ );
139
+ });
@@ -0,0 +1 @@
1
+ export { IconButton, type IconButtonProps, type IconButtonSize } from "./icon-button";
@@ -0,0 +1 @@
1
+ export { Input, type InputProps } from "./input";
@@ -0,0 +1,44 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Input } from "./input";
3
+
4
+ const meta = {
5
+ title: "Core/Input",
6
+ component: Input,
7
+ tags: ["autodocs"],
8
+ args: { placeholder: "you@example.com" },
9
+ argTypes: {
10
+ type: {
11
+ description:
12
+ "HTML input type — controls browser behaviour, autocomplete, and mobile keyboard.",
13
+ control: { type: "select" },
14
+ options: ["text", "email", "password", "number", "tel", "url", "search", "date", "file"],
15
+ table: { category: "Behavior" },
16
+ },
17
+ placeholder: {
18
+ description: "Placeholder hint (should show an example, end with …).",
19
+ control: "text",
20
+ table: { category: "Content" },
21
+ },
22
+ disabled: {
23
+ description: "Disables the input — cursor-not-allowed, reduced opacity, muted background.",
24
+ control: "boolean",
25
+ table: { category: "State" },
26
+ },
27
+ "aria-invalid": {
28
+ description: "Marks the field invalid — shows destructive ring / border.",
29
+ control: "boolean",
30
+ table: { category: "State" },
31
+ },
32
+ className: {
33
+ description: "Extra Tailwind classes merged via cn().",
34
+ control: "text",
35
+ table: { category: "Appearance" },
36
+ },
37
+ },
38
+ } satisfies Meta<typeof Input>;
39
+ export default meta;
40
+ type Story = StoryObj<typeof meta>;
41
+
42
+ export const Default: Story = {};
43
+ export const Invalid: Story = { args: { "aria-invalid": true, defaultValue: "not-an-email" } };
44
+ export const Disabled: Story = { args: { disabled: true, value: "Disabled" } };
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { Input } from "./input";
5
+
6
+ describe("Input", () => {
7
+ it("accepts typed text", async () => {
8
+ render(<Input aria-label="email" />);
9
+ const el = screen.getByLabelText<HTMLInputElement>("email");
10
+ await userEvent.type(el, "hi");
11
+ expect(el.value).toBe("hi");
12
+ });
13
+
14
+ it("is disabled when disabled prop is set", () => {
15
+ render(<Input aria-label="email" disabled />);
16
+ const el = screen.getByLabelText<HTMLInputElement>("email");
17
+ expect(el).toBeDisabled();
18
+ });
19
+ });
@@ -0,0 +1,26 @@
1
+ import { forwardRef, type InputHTMLAttributes } from "react";
2
+ import { cn } from "../../lib/cn";
3
+
4
+ export type InputProps = InputHTMLAttributes<HTMLInputElement>;
5
+
6
+ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
7
+ { className, type = "text", ...props },
8
+ ref,
9
+ ) {
10
+ return (
11
+ <input
12
+ ref={ref}
13
+ type={type}
14
+ className={cn(
15
+ "flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-[color,background-color,border-color,box-shadow] duration-fast ease-standard",
16
+ "placeholder:text-muted-foreground",
17
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
18
+ "disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-muted disabled:border-border",
19
+ "aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive",
20
+ "file:border-0 file:bg-transparent file:text-sm file:font-medium",
21
+ className,
22
+ )}
23
+ {...props}
24
+ />
25
+ );
26
+ });
@@ -0,0 +1,11 @@
1
+ export {
2
+ InputGroup,
3
+ InputGroupAddon,
4
+ InputGroupButton,
5
+ InputGroupText,
6
+ InputGroupInput,
7
+ InputGroupTextarea,
8
+ inputGroupVariants,
9
+ inputGroupAddonVariants,
10
+ type InputGroupProps,
11
+ } from "./input-group";
@@ -0,0 +1,87 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Search } from "lucide-react";
3
+ import {
4
+ InputGroup,
5
+ InputGroupAddon,
6
+ InputGroupButton,
7
+ InputGroupInput,
8
+ InputGroupText,
9
+ } from "./input-group";
10
+ const meta = {
11
+ title: "Forms/InputGroup",
12
+ component: InputGroup,
13
+ tags: ["autodocs"],
14
+ argTypes: {
15
+ variant: {
16
+ description:
17
+ "`outline` — bordered form-field look (default). `surface` — soft filled composer look with no hard border. `card` — `bg-card` well for nesting inside an already-tinted outer frame (the double card, #254); its fill is theme-driven, not universally white — raised on light themes, recessed on dark.",
18
+ control: { type: "radio" },
19
+ options: ["outline", "surface", "card"],
20
+ table: { category: "Appearance" },
21
+ },
22
+ className: {
23
+ description: "Additional CSS classes applied to the root element.",
24
+ control: "text",
25
+ table: { category: "Appearance" },
26
+ },
27
+ children: {
28
+ description: "Compose with InputGroupAddon, InputGroupInput, InputGroupButton, etc.",
29
+ control: false,
30
+ table: { category: "Content" },
31
+ },
32
+ },
33
+ } satisfies Meta<typeof InputGroup>;
34
+ export default meta;
35
+ type Story = StoryObj<typeof meta>;
36
+ export const Search_: Story = {
37
+ name: "Search",
38
+ render: () => (
39
+ <InputGroup className="max-w-sm">
40
+ <InputGroupAddon>
41
+ <Search />
42
+ </InputGroupAddon>
43
+ <InputGroupInput placeholder="Search…" />
44
+ </InputGroup>
45
+ ),
46
+ };
47
+ export const WithButton: Story = {
48
+ render: () => (
49
+ <InputGroup className="max-w-sm">
50
+ <InputGroupAddon>
51
+ <InputGroupText>$</InputGroupText>
52
+ </InputGroupAddon>
53
+ <InputGroupInput placeholder="0.00" />
54
+ <InputGroupAddon align="inline-end">
55
+ <InputGroupButton size="sm">USD</InputGroupButton>
56
+ </InputGroupAddon>
57
+ </InputGroup>
58
+ ),
59
+ };
60
+ // The composer look (#194, research 08 §C.1): soft `bg-surface-muted` fill +
61
+ // the focus-within ring — no hard border. Used by `PromptInput` in @elabs-ai/components-ai.
62
+ export const Surface: Story = {
63
+ render: () => (
64
+ <InputGroup variant="surface" className="max-w-sm">
65
+ <InputGroupAddon>
66
+ <Search />
67
+ </InputGroupAddon>
68
+ <InputGroupInput placeholder="Ask anything…" />
69
+ </InputGroup>
70
+ ),
71
+ };
72
+ // The "double card" well (#254): nest a `card` InputGroup inside an
73
+ // already-tinted outer frame — `PromptInput`'s `tone="card"` prop wires this
74
+ // into the AI composer. The well's fill is theme-driven (raised on light
75
+ // themes, recessed on dark), not universally white.
76
+ export const Card: Story = {
77
+ render: () => (
78
+ <div className="max-w-sm rounded-xl bg-surface-muted p-1.5">
79
+ <InputGroup variant="card">
80
+ <InputGroupAddon>
81
+ <Search />
82
+ </InputGroupAddon>
83
+ <InputGroupInput placeholder="Ask anything…" />
84
+ </InputGroup>
85
+ </div>
86
+ ),
87
+ };
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render } from "@testing-library/react";
3
+ import { InputGroup } from "./input-group";
4
+
5
+ describe("InputGroup", () => {
6
+ // Regression lock for #8: the auto-height selector must match ANY descendant
7
+ // textarea (so it works through PromptInputBody's `display:contents` wrapper),
8
+ // not just a direct child. jsdom can't compute `:has()` layout, so we lock the
9
+ // class contract instead.
10
+ it("uses a descendant `has-[textarea]` selector (not direct-child) to auto-grow", () => {
11
+ const { container } = render(
12
+ <InputGroup>
13
+ <div className="contents">
14
+ <textarea data-slot="input-group-control" />
15
+ </div>
16
+ </InputGroup>,
17
+ );
18
+ const group = container.querySelector('[data-slot="input-group"]') as HTMLElement;
19
+ expect(group).not.toBeNull();
20
+ expect(group.className).toContain("has-[textarea]:h-auto");
21
+ expect(group.className).not.toContain("has-[>textarea]");
22
+ });
23
+
24
+ // #194 (research 08 §C.1/§D): the composer variant separates by soft fill +
25
+ // focus ring, never a hard border; the default stays the form-field outline.
26
+ it("variant='surface' uses the soft fill with no border; default keeps border-input", () => {
27
+ const { container } = render(<InputGroup variant="surface" />);
28
+ const surface = container.querySelector('[data-slot="input-group"]') as HTMLElement;
29
+ expect(surface.className.split(" ")).toContain("bg-surface-muted");
30
+ expect(surface.className.split(" ")).not.toContain("border");
31
+ expect(surface.className).not.toContain("border-input");
32
+
33
+ const { container: c2 } = render(<InputGroup />);
34
+ const outline = c2.querySelector('[data-slot="input-group"]') as HTMLElement;
35
+ expect(outline.className.split(" ")).toContain("border-input");
36
+ });
37
+
38
+ // #254: the "double card" needs a `bg-card` well with no border of its own
39
+ // (the outer frame supplies the tint; the well's own fill is theme-driven,
40
+ // not universally white — see the JSDoc on `inputGroupVariants`).
41
+ it("variant='card' uses bg-card with no border", () => {
42
+ const { container } = render(<InputGroup variant="card" />);
43
+ const card = container.querySelector('[data-slot="input-group"]') as HTMLElement;
44
+ expect(card.className.split(" ")).toContain("bg-card");
45
+ expect(card.className.split(" ")).not.toContain("border");
46
+ expect(card.className).not.toContain("border-input");
47
+ });
48
+ });