@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,178 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, within } from "storybook/test";
3
+ import { LinkPreviewCard } from "./link-preview";
4
+
5
+ const meta = {
6
+ title: "Core/LinkPreviewCard",
7
+ component: LinkPreviewCard,
8
+ tags: ["autodocs"],
9
+ parameters: { layout: "centered" },
10
+ argTypes: {
11
+ title: {
12
+ description: "Page title — the primary text, 1–2 lines.",
13
+ control: "text",
14
+ table: { category: "Content" },
15
+ },
16
+ description: {
17
+ description: "Short description / Open-Graph description. Omit for title+domain only.",
18
+ control: "text",
19
+ table: { category: "Content" },
20
+ },
21
+ image: {
22
+ description:
23
+ 'Absolute URL of the thumbnail image (consumer-provided; decorative, alt is always "").',
24
+ control: "text",
25
+ table: { category: "Content" },
26
+ },
27
+ domain: {
28
+ description: 'Domain label shown with the globe icon, e.g. `"github.com"`.',
29
+ control: "text",
30
+ table: { category: "Content" },
31
+ },
32
+ href: {
33
+ description: "When set, the card becomes a keyboard-focusable stretched link.",
34
+ control: "text",
35
+ table: { category: "Behavior" },
36
+ },
37
+ loading: {
38
+ description: 'Render skeleton placeholders — sets `aria-busy="true"`.',
39
+ control: "boolean",
40
+ table: { category: "State" },
41
+ },
42
+ className: {
43
+ description: "Extra Tailwind classes merged via cn().",
44
+ control: "text",
45
+ table: { category: "Appearance" },
46
+ },
47
+ },
48
+ } satisfies Meta<typeof LinkPreviewCard>;
49
+
50
+ export default meta;
51
+ type Story = StoryObj<typeof meta>;
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Loaded — full state (title + description + thumbnail + domain + href)
55
+ // ---------------------------------------------------------------------------
56
+
57
+ export const Default: Story = {
58
+ render: () => (
59
+ <LinkPreviewCard
60
+ className="w-96"
61
+ title="React documentation – Quick start"
62
+ description="Learn the basics of React: components, props, state, and how they fit together to build interactive UIs."
63
+ domain="react.dev"
64
+ image="https://react.dev/images/og-learn.png"
65
+ href="https://react.dev/learn"
66
+ />
67
+ ),
68
+ play: async ({ canvasElement }) => {
69
+ const canvas = within(canvasElement);
70
+ // Link is present and has the right href
71
+ const link = canvas.getByRole("link", { name: /react documentation/i });
72
+ await expect(link).toBeInTheDocument();
73
+ await expect(link).toHaveAttribute("href", "https://react.dev/learn");
74
+ await expect(link).toHaveAttribute("rel", "noopener noreferrer");
75
+ // Domain text is visible
76
+ await expect(canvas.getByText("react.dev")).toBeInTheDocument();
77
+ // Thumbnail image is decorative (aria-hidden)
78
+ const img = canvasElement.querySelector("img");
79
+ await expect(img).toHaveAttribute("aria-hidden", "true");
80
+ await expect(img).toHaveAttribute("alt", "");
81
+ },
82
+ };
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // No image — title + description + domain, thumbnail area absent
86
+ // ---------------------------------------------------------------------------
87
+
88
+ export const NoImage: Story = {
89
+ render: () => (
90
+ <LinkPreviewCard
91
+ className="w-96"
92
+ title="GitHub – elabs/elabs-components"
93
+ description="Source-owned, token-driven React component system for internal apps and prototypes."
94
+ domain="github.com"
95
+ href="https://github.com/elabs/elabs-components"
96
+ />
97
+ ),
98
+ play: async ({ canvasElement }) => {
99
+ // No <img> element rendered when image prop is absent
100
+ const img = canvasElement.querySelector("img");
101
+ await expect(img).toBeNull();
102
+ },
103
+ };
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Title + domain only — no description
107
+ // ---------------------------------------------------------------------------
108
+
109
+ export const TitleAndDomainOnly: Story = {
110
+ render: () => (
111
+ <LinkPreviewCard
112
+ className="w-96"
113
+ title="Tailwind CSS – Utility-first CSS framework"
114
+ domain="tailwindcss.com"
115
+ href="https://tailwindcss.com"
116
+ />
117
+ ),
118
+ };
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Loading — skeleton state
122
+ // ---------------------------------------------------------------------------
123
+
124
+ export const Loading: Story = {
125
+ render: () => <LinkPreviewCard className="w-96" loading />,
126
+ play: async ({ canvasElement }) => {
127
+ const card = canvasElement.querySelector("[aria-busy]");
128
+ await expect(card).toHaveAttribute("aria-busy", "true");
129
+ // No real content rendered while loading
130
+ const link = canvasElement.querySelector("a");
131
+ await expect(link).toBeNull();
132
+ },
133
+ };
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // No href — static / non-interactive card
137
+ // ---------------------------------------------------------------------------
138
+
139
+ export const Static: Story = {
140
+ render: () => (
141
+ <LinkPreviewCard
142
+ className="w-96"
143
+ title="MDN Web Docs – HTML: HyperText Markup Language"
144
+ description="HTML is the standard markup language for Web pages. With HTML you can create your own Website."
145
+ domain="developer.mozilla.org"
146
+ image="https://developer.mozilla.org/mdn-social-share.png"
147
+ />
148
+ ),
149
+ play: async ({ canvasElement }) => {
150
+ // Static card: no <a> link present
151
+ const link = canvasElement.querySelector("a");
152
+ await expect(link).toBeNull();
153
+ },
154
+ };
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Empty / minimal — no props except className: degrades gracefully
158
+ // ---------------------------------------------------------------------------
159
+
160
+ export const Empty: Story = {
161
+ render: () => <LinkPreviewCard className="w-96" />,
162
+ };
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // Long content — line-clamp kicks in, no overflow
166
+ // ---------------------------------------------------------------------------
167
+
168
+ export const LongContent: Story = {
169
+ render: () => (
170
+ <LinkPreviewCard
171
+ className="w-96"
172
+ title="This is a very long page title that will wrap across multiple lines and should be clamped after two lines to prevent the card from growing too tall in a feed or sidebar context"
173
+ description="And here is an equally long description — the kind you might get from an overly verbose Open Graph meta tag that tries to include every detail of the page. It should be clamped at two lines."
174
+ domain="example-with-a-very-long-subdomain.internal.enterprise.example.com"
175
+ href="https://example.com"
176
+ />
177
+ ),
178
+ };
@@ -0,0 +1,123 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { LinkPreview, LinkPreviewCard } from "./link-preview";
4
+
5
+ describe("LinkPreviewCard", () => {
6
+ // -------------------------------------------------------------------------
7
+ // Loaded state
8
+ // -------------------------------------------------------------------------
9
+
10
+ it("renders title, description, and domain", () => {
11
+ render(
12
+ <LinkPreviewCard
13
+ title="React – Quick start"
14
+ description="Learn the basics."
15
+ domain="react.dev"
16
+ />,
17
+ );
18
+ expect(screen.getByText("React – Quick start")).toBeInTheDocument();
19
+ expect(screen.getByText("Learn the basics.")).toBeInTheDocument();
20
+ expect(screen.getByText("react.dev")).toBeInTheDocument();
21
+ });
22
+
23
+ it("renders a real <a> link when href is provided", () => {
24
+ render(
25
+ <LinkPreviewCard title="React docs" href="https://react.dev/learn" domain="react.dev" />,
26
+ );
27
+ const link = screen.getByRole("link", { name: /react docs/i });
28
+ expect(link).toBeInTheDocument();
29
+ expect(link).toHaveAttribute("href", "https://react.dev/learn");
30
+ expect(link).toHaveAttribute("rel", "noopener noreferrer");
31
+ expect(link).toHaveAttribute("target", "_blank");
32
+ });
33
+
34
+ it("renders no <a> element when href is absent", () => {
35
+ render(<LinkPreviewCard title="Static card" domain="example.com" />);
36
+ expect(screen.queryByRole("link")).toBeNull();
37
+ });
38
+
39
+ // -------------------------------------------------------------------------
40
+ // Loading state
41
+ // -------------------------------------------------------------------------
42
+
43
+ it("sets aria-busy when loading", () => {
44
+ render(<LinkPreviewCard loading data-testid="card" />);
45
+ const card = screen.getByTestId("card");
46
+ expect(card).toHaveAttribute("aria-busy", "true");
47
+ });
48
+
49
+ it("does not render real content when loading", () => {
50
+ render(
51
+ <LinkPreviewCard
52
+ loading
53
+ title="Should not appear"
54
+ description="Hidden"
55
+ domain="example.com"
56
+ />,
57
+ );
58
+ expect(screen.queryByText("Should not appear")).toBeNull();
59
+ expect(screen.queryByRole("link")).toBeNull();
60
+ });
61
+
62
+ it("does not set aria-busy when not loading", () => {
63
+ render(<LinkPreviewCard title="Loaded" data-testid="card" />);
64
+ const card = screen.getByTestId("card");
65
+ // aria-busy is undefined (not present) when not loading
66
+ expect(card).not.toHaveAttribute("aria-busy");
67
+ });
68
+
69
+ // -------------------------------------------------------------------------
70
+ // No image
71
+ // -------------------------------------------------------------------------
72
+
73
+ it("renders no <img> when image prop is absent", () => {
74
+ const { container } = render(<LinkPreviewCard title="No image" />);
75
+ expect(container.querySelector("img")).toBeNull();
76
+ });
77
+
78
+ it("renders a decorative <img> with alt='' when image is provided", () => {
79
+ const { container } = render(
80
+ <LinkPreviewCard title="Has image" image="https://example.com/og.png" />,
81
+ );
82
+ const img = container.querySelector("img");
83
+ expect(img).not.toBeNull();
84
+ expect(img).toHaveAttribute("alt", "");
85
+ expect(img).toHaveAttribute("aria-hidden", "true");
86
+ expect(img).toHaveAttribute("src", "https://example.com/og.png");
87
+ });
88
+
89
+ // -------------------------------------------------------------------------
90
+ // Composability
91
+ // -------------------------------------------------------------------------
92
+
93
+ it("forwards className and extra props to the root element", () => {
94
+ render(
95
+ <LinkPreviewCard
96
+ title="Composable"
97
+ className="my-custom-class"
98
+ data-testid="card"
99
+ aria-label="Link preview"
100
+ />,
101
+ );
102
+ const card = screen.getByTestId("card");
103
+ expect(card).toHaveClass("my-custom-class");
104
+ expect(card).toHaveAttribute("aria-label", "Link preview");
105
+ });
106
+
107
+ it("renders children inside the card", () => {
108
+ render(
109
+ <LinkPreviewCard title="With children">
110
+ <span data-testid="child">Extra content</span>
111
+ </LinkPreviewCard>,
112
+ );
113
+ expect(screen.getByTestId("child")).toBeInTheDocument();
114
+ });
115
+
116
+ // -------------------------------------------------------------------------
117
+ // LinkPreview alias
118
+ // -------------------------------------------------------------------------
119
+
120
+ it("LinkPreview is the same component as LinkPreviewCard", () => {
121
+ expect(LinkPreview).toBe(LinkPreviewCard);
122
+ });
123
+ });
@@ -0,0 +1,194 @@
1
+ "use client";
2
+
3
+ import { forwardRef, type HTMLAttributes } from "react";
4
+ import { GlobeIcon } from "lucide-react";
5
+ import { cn } from "../../lib/cn";
6
+ import { Skeleton } from "../skeleton/skeleton";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Public types
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export interface LinkPreviewCardProps extends HTMLAttributes<HTMLDivElement> {
13
+ /** Page title — the primary text, 1–2 lines. */
14
+ title?: string;
15
+ /** Short description / Open-Graph description. Omit for title+domain only. */
16
+ description?: string;
17
+ /**
18
+ * Absolute URL of the thumbnail image (consumer-provided; the library never
19
+ * fetches). Decorative — `alt` is always `""`. Omit to hide the thumbnail
20
+ * area cleanly (no broken-image placeholder).
21
+ */
22
+ image?: string;
23
+ /**
24
+ * Domain label shown with the globe icon, e.g. `"github.com"`. Wrapped in
25
+ * `translate="no"` so browsers never auto-translate identifiers.
26
+ */
27
+ domain?: string;
28
+ /**
29
+ * When set, the entire card becomes a keyboard-focusable link (stretched-link
30
+ * pattern). The visible focus ring is placed on the card via `focus-within`.
31
+ * Prefer a full absolute URL so the `<a>` is meaningful.
32
+ */
33
+ href?: string;
34
+ /**
35
+ * Render skeleton placeholders in lieu of real content. Sets `aria-busy="true"`
36
+ * on the card root so assistive technology knows the content is pending.
37
+ */
38
+ loading?: boolean;
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Internal helpers
43
+ // ---------------------------------------------------------------------------
44
+
45
+ /** Thumbnail — hidden when `image` is absent (no broken-icon placeholder). */
46
+ function Thumbnail({ image }: { image?: string }) {
47
+ if (!image) return null;
48
+ return (
49
+ <div className="relative h-full w-28 shrink-0 overflow-hidden rounded-e-lg">
50
+ {/* explicit width/height prevent CLS; object-cover fills the container */}
51
+ <img
52
+ src={image}
53
+ alt="" // decorative; title carries meaning
54
+ aria-hidden="true"
55
+ width={112}
56
+ height={112}
57
+ loading="lazy"
58
+ className="h-full w-full object-cover"
59
+ />
60
+ </div>
61
+ );
62
+ }
63
+
64
+ /** Skeleton layout matching the card shape. */
65
+ function SkeletonLayout() {
66
+ return (
67
+ <div className="flex min-h-[5rem] gap-3 p-3">
68
+ <div className="flex min-w-0 flex-1 flex-col gap-2 py-0.5">
69
+ {/* domain row */}
70
+ <Skeleton className="h-3 w-24" />
71
+ {/* title — two lines */}
72
+ <Skeleton className="h-4 w-full" />
73
+ <Skeleton className="h-4 w-3/4" />
74
+ {/* description */}
75
+ <Skeleton className="mt-1 h-3 w-full" />
76
+ <Skeleton className="h-3 w-5/6" />
77
+ </div>
78
+ {/* thumbnail placeholder */}
79
+ <Skeleton className="h-20 w-28 shrink-0 rounded-md" />
80
+ </div>
81
+ );
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // LinkPreviewCard
86
+ // ---------------------------------------------------------------------------
87
+
88
+ /**
89
+ * A rich, presentational link-preview card (unfurled URL / bookmark block).
90
+ * Renders favicon/domain, title, description, and an optional thumbnail from
91
+ * **consumer-provided** metadata — the library never fetches.
92
+ *
93
+ * States: `loading` (skeleton), loaded (full), no-image, no-description
94
+ * (title + domain only), and empty/error (degrades to a plain placeholder).
95
+ *
96
+ * Pass `href` to make the whole card a keyboard-focusable stretched link.
97
+ * Use `loading` for the skeleton state; set `aria-busy` is applied automatically.
98
+ */
99
+ export const LinkPreviewCard = forwardRef<HTMLDivElement, LinkPreviewCardProps>(
100
+ function LinkPreviewCard(
101
+ { className, title, description, image, domain, href, loading = false, children, ...props },
102
+ ref,
103
+ ) {
104
+ return (
105
+ <div
106
+ ref={ref}
107
+ aria-busy={loading || undefined}
108
+ className={cn(
109
+ // Base card chrome — rounded border, card surface, subtle shadow
110
+ "relative overflow-hidden rounded-lg border bg-card text-card-foreground shadow-sm",
111
+ // Stretched-link focus ring: when `href` is present the inner <a>
112
+ // covers the whole card; the ring is painted on the card container via
113
+ // focus-within so the outer border gets the visible ring, not the
114
+ // hidden absolutely-positioned link.
115
+ // When `href` is present: stretched-link focus ring (via focus-within on
116
+ // the container, not the <a>) + hover lift + motion-tokened transition.
117
+ href &&
118
+ "cursor-pointer transition-[translate,box-shadow,border-color] duration-fast ease-standard focus-within:outline-none focus-within:ring-2 focus-within:ring-ring hover:-translate-y-0.5 hover:border-ring/40 hover:shadow-md motion-reduce:hover:translate-y-0",
119
+ className,
120
+ )}
121
+ {...props}
122
+ >
123
+ {loading ? (
124
+ <SkeletonLayout />
125
+ ) : (
126
+ <div className="flex min-h-[5rem] gap-0">
127
+ {/* Main content */}
128
+ <div className="flex min-w-0 flex-1 flex-col justify-center gap-1.5 p-3">
129
+ {/* Domain row */}
130
+ {domain && (
131
+ <p className="flex items-center gap-1.5 text-meta text-muted-foreground">
132
+ <GlobeIcon aria-hidden="true" className="size-3 shrink-0" />
133
+ {/* translate="no" so browsers never auto-translate domain identifiers */}
134
+ <span className="truncate" translate="no">
135
+ {domain}
136
+ </span>
137
+ </p>
138
+ )}
139
+
140
+ {/* Title */}
141
+ {title && (
142
+ <p className="line-clamp-2 min-w-0 text-body font-medium leading-snug">
143
+ {href ? (
144
+ /*
145
+ * Stretched-link pattern: `<a>` is absolutely positioned to
146
+ * cover the entire card (after:absolute after:inset-0). The
147
+ * visible focus ring lives on the card container via
148
+ * focus-within (above). `focus-visible:outline-none` removes
149
+ * the browser's own ring on the link so it doesn't double up.
150
+ */
151
+ <a
152
+ href={href}
153
+ target="_blank"
154
+ rel="noopener noreferrer"
155
+ className="after:absolute after:inset-0 focus-visible:outline-none"
156
+ >
157
+ {title}
158
+ </a>
159
+ ) : (
160
+ title
161
+ )}
162
+ </p>
163
+ )}
164
+
165
+ {/* Description */}
166
+ {description && (
167
+ <p className="line-clamp-2 min-w-0 text-caption text-muted-foreground">
168
+ {description}
169
+ </p>
170
+ )}
171
+
172
+ {children}
173
+ </div>
174
+
175
+ {/* Thumbnail — conditionally rendered */}
176
+ <Thumbnail image={image} />
177
+ </div>
178
+ )}
179
+ </div>
180
+ );
181
+ },
182
+ );
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // LinkPreview alias
186
+ // ---------------------------------------------------------------------------
187
+
188
+ /**
189
+ * Convenience alias for `LinkPreviewCard`. Identical component — use whichever
190
+ * name reads more naturally at the call site.
191
+ *
192
+ * `LinkPreviewCard` is the canonical export; `LinkPreview` is the alias.
193
+ */
194
+ export const LinkPreview = LinkPreviewCard;
@@ -0,0 +1 @@
1
+ export { ListEditor, type ListEditorProps } from "./list-editor";
@@ -0,0 +1,119 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect } from "storybook/test";
3
+ import { useState } from "react";
4
+ import { ListEditor } from "./list-editor";
5
+
6
+ const meta = {
7
+ title: "Forms/ListEditor",
8
+ component: ListEditor,
9
+ tags: ["autodocs"],
10
+ args: {
11
+ placeholder: "--flag=value…",
12
+ },
13
+ argTypes: {
14
+ value: {
15
+ description: "Controlled list of strings.",
16
+ control: false,
17
+ table: { category: "State" },
18
+ },
19
+ defaultValue: {
20
+ description: "Uncontrolled initial list of strings.",
21
+ control: false,
22
+ table: { category: "State" },
23
+ },
24
+ reorderable: {
25
+ description: "Whether rows can be reordered via move-up/move-down buttons.",
26
+ control: "boolean",
27
+ table: { category: "Behavior" },
28
+ },
29
+ max: {
30
+ description: "Maximum number of rows allowed.",
31
+ control: "number",
32
+ table: { category: "Behavior" },
33
+ },
34
+ placeholder: {
35
+ description: "Placeholder shown in each row's text field.",
36
+ control: "text",
37
+ table: { category: "Content" },
38
+ },
39
+ disabled: {
40
+ description: "Disables the editor.",
41
+ control: "boolean",
42
+ table: { category: "State" },
43
+ },
44
+ addLabel: {
45
+ description: 'Label for the "add item" control.',
46
+ control: "text",
47
+ table: { category: "Content" },
48
+ },
49
+ onValueChange: {
50
+ description: "Called when the list changes.",
51
+ control: false,
52
+ table: { category: "Behavior" },
53
+ },
54
+ className: {
55
+ description: "Additional CSS classes applied to the root element.",
56
+ control: "text",
57
+ table: { category: "Appearance" },
58
+ },
59
+ },
60
+ } satisfies Meta<typeof ListEditor>;
61
+ export default meta;
62
+ type Story = StoryObj<typeof meta>;
63
+
64
+ export const Default: Story = {
65
+ args: { defaultValue: ["--verbose", "--output=json"] },
66
+ };
67
+
68
+ export const Controlled: Story = {
69
+ render: () => {
70
+ // eslint-disable-next-line react-hooks/rules-of-hooks
71
+ const [items, setItems] = useState(["one", "two"]);
72
+ return (
73
+ <div className="flex flex-col gap-2 max-w-sm">
74
+ <ListEditor value={items} onValueChange={setItems} placeholder="Item…" />
75
+ <p className="text-body text-muted-foreground">Items: {items.join(", ") || "none"}</p>
76
+ </div>
77
+ );
78
+ },
79
+ };
80
+
81
+ export const NonReorderable: Story = {
82
+ args: { defaultValue: ["first", "second"], reorderable: false },
83
+ };
84
+
85
+ /** Adds a row, types into it, removes a row, and moves a row up — all via real buttons. */
86
+ export const AddEditRemoveMove: Story = {
87
+ args: { defaultValue: ["alpha", "beta"] },
88
+ play: async ({ canvas, userEvent }) => {
89
+ await userEvent.click(canvas.getByRole("button", { name: /add item/i }));
90
+ const rowInputs = canvas.getAllByRole("textbox");
91
+ await expect(rowInputs).toHaveLength(3);
92
+ await userEvent.type(rowInputs[2]!, "gamma");
93
+ await expect(rowInputs[2]).toHaveValue("gamma");
94
+
95
+ // Move the last row up one position via the keyboard-operable button.
96
+ const moveUpButtons = canvas.getAllByRole("button", { name: /move item 3 up/i });
97
+ await userEvent.click(moveUpButtons[0]!);
98
+ const afterMove = canvas.getAllByRole("textbox");
99
+ await expect(afterMove[1]).toHaveValue("gamma");
100
+
101
+ await userEvent.click(canvas.getAllByRole("button", { name: /remove item 1/i })[0]!);
102
+ await expect(canvas.getAllByRole("textbox")).toHaveLength(2);
103
+ },
104
+ };
105
+
106
+ export const WithMax: Story = {
107
+ args: { defaultValue: ["one", "two"], max: 2 },
108
+ };
109
+
110
+ export const Disabled: Story = {
111
+ args: { defaultValue: ["locked"], disabled: true },
112
+ };
113
+
114
+ export const Empty: Story = {
115
+ args: { defaultValue: [] },
116
+ play: async ({ canvas }) => {
117
+ await expect(canvas.getByText("No items yet.")).toBeInTheDocument();
118
+ },
119
+ };