@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,618 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, waitFor, within, userEvent } from "storybook/test";
3
+ import { useState } from "react";
4
+ import { Folder, File, FileText, Database, Server } from "lucide-react";
5
+ import { Badge } from "../badge";
6
+ import { Tree, type TreeNode } from "./tree";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Shared sample data
10
+ // ---------------------------------------------------------------------------
11
+
12
+ const fileTree: TreeNode[] = [
13
+ {
14
+ id: "src",
15
+ label: "src",
16
+ icon: <Folder className="size-4 text-muted-foreground" />,
17
+ children: [
18
+ {
19
+ id: "components",
20
+ label: "components",
21
+ icon: <Folder className="size-4 text-muted-foreground" />,
22
+ children: [
23
+ {
24
+ id: "button",
25
+ label: "button.tsx",
26
+ icon: <FileText className="size-4 text-muted-foreground" />,
27
+ },
28
+ {
29
+ id: "tree-tsx",
30
+ label: "tree.tsx",
31
+ icon: <FileText className="size-4 text-muted-foreground" />,
32
+ },
33
+ ],
34
+ },
35
+ {
36
+ id: "lib",
37
+ label: "lib",
38
+ icon: <Folder className="size-4 text-muted-foreground" />,
39
+ children: [
40
+ {
41
+ id: "cn",
42
+ label: "cn.ts",
43
+ icon: <File className="size-4 text-muted-foreground" />,
44
+ },
45
+ ],
46
+ },
47
+ {
48
+ id: "index",
49
+ label: "index.ts",
50
+ icon: <File className="size-4 text-muted-foreground" />,
51
+ },
52
+ ],
53
+ },
54
+ {
55
+ id: "public",
56
+ label: "public",
57
+ icon: <Folder className="size-4 text-muted-foreground" />,
58
+ children: [
59
+ {
60
+ id: "favicon",
61
+ label: "favicon.ico",
62
+ icon: <File className="size-4 text-muted-foreground" />,
63
+ },
64
+ ],
65
+ },
66
+ {
67
+ id: "package-json",
68
+ label: "package.json",
69
+ icon: <File className="size-4 text-muted-foreground" />,
70
+ },
71
+ ];
72
+
73
+ const orgTree: TreeNode[] = [
74
+ {
75
+ id: "engineering",
76
+ label: "Engineering",
77
+ icon: <Server className="size-4 text-muted-foreground" />,
78
+ children: [
79
+ {
80
+ id: "frontend",
81
+ label: "Frontend",
82
+ children: [
83
+ { id: "alice", label: "Alice" },
84
+ { id: "bob", label: "Bob" },
85
+ ],
86
+ },
87
+ {
88
+ id: "backend",
89
+ label: "Backend",
90
+ children: [
91
+ { id: "charlie", label: "Charlie" },
92
+ { id: "dana", label: "Dana", disabled: true },
93
+ ],
94
+ },
95
+ ],
96
+ },
97
+ {
98
+ id: "data",
99
+ label: "Data",
100
+ icon: <Database className="size-4 text-muted-foreground" />,
101
+ children: [
102
+ { id: "eve", label: "Eve" },
103
+ { id: "frank", label: "Frank" },
104
+ ],
105
+ },
106
+ ];
107
+
108
+ // The same file tree, with a trailing size badge on each file leaf (#369) —
109
+ // `TreeNode.accessory` renders it as a SIBLING of the label, so it never joins
110
+ // the row's accessible name and never triggers select/expand.
111
+ const fileTreeWithSizes: TreeNode[] = [
112
+ {
113
+ id: "src",
114
+ label: "src",
115
+ icon: <Folder className="size-4 text-muted-foreground" />,
116
+ children: [
117
+ {
118
+ id: "components",
119
+ label: "components",
120
+ icon: <Folder className="size-4 text-muted-foreground" />,
121
+ children: [
122
+ {
123
+ id: "button",
124
+ label: "button.tsx",
125
+ icon: <FileText className="size-4 text-muted-foreground" />,
126
+ accessory: <Badge variant="outline">2.1 KB</Badge>,
127
+ },
128
+ {
129
+ id: "tree-tsx",
130
+ label: "tree.tsx",
131
+ icon: <FileText className="size-4 text-muted-foreground" />,
132
+ accessory: <Badge variant="outline">18.4 KB</Badge>,
133
+ },
134
+ ],
135
+ },
136
+ {
137
+ id: "index",
138
+ label: "index.ts",
139
+ icon: <File className="size-4 text-muted-foreground" />,
140
+ accessory: <Badge variant="outline">0.4 KB</Badge>,
141
+ },
142
+ ],
143
+ },
144
+ {
145
+ id: "package-json",
146
+ label: "package.json",
147
+ icon: <File className="size-4 text-muted-foreground" />,
148
+ accessory: <Badge variant="outline">1.2 KB</Badge>,
149
+ },
150
+ ];
151
+
152
+ // ---------------------------------------------------------------------------
153
+ // 5 000-node flat tree for the virtualization story
154
+ // ---------------------------------------------------------------------------
155
+
156
+ function buildLargeTree(count: number): TreeNode[] {
157
+ const nodes: TreeNode[] = [];
158
+ for (let i = 0; i < count; i++) {
159
+ nodes.push({ id: `node-${i}`, label: `Node ${i + 1}` });
160
+ }
161
+ return nodes;
162
+ }
163
+
164
+ const LARGE_NODES = buildLargeTree(5000);
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // Lazy-loading demo data
168
+ // ---------------------------------------------------------------------------
169
+
170
+ function makeLazyRoot(): TreeNode[] {
171
+ return [
172
+ {
173
+ id: "root-a",
174
+ label: "Category A",
175
+ hasChildren: true,
176
+ icon: <Folder className="size-4 text-muted-foreground" />,
177
+ },
178
+ {
179
+ id: "root-b",
180
+ label: "Category B",
181
+ hasChildren: true,
182
+ icon: <Folder className="size-4 text-muted-foreground" />,
183
+ },
184
+ {
185
+ id: "root-c",
186
+ label: "Category C (fails to load)",
187
+ hasChildren: true,
188
+ icon: <Folder className="size-4 text-muted-foreground" />,
189
+ },
190
+ {
191
+ id: "root-d",
192
+ label: "Static leaf",
193
+ icon: <File className="size-4 text-muted-foreground" />,
194
+ },
195
+ ];
196
+ }
197
+
198
+ async function fakeFetch(nodeId: string): Promise<TreeNode[]> {
199
+ await new Promise((resolve) => setTimeout(resolve, 800));
200
+ if (nodeId === "root-c") throw new Error("Network error");
201
+ return [
202
+ {
203
+ id: `${nodeId}-child-1`,
204
+ label: `${nodeId} / child 1`,
205
+ hasChildren: true,
206
+ icon: <Folder className="size-4 text-muted-foreground" />,
207
+ },
208
+ {
209
+ id: `${nodeId}-child-2`,
210
+ label: `${nodeId} / child 2`,
211
+ icon: <FileText className="size-4 text-muted-foreground" />,
212
+ },
213
+ {
214
+ id: `${nodeId}-child-3`,
215
+ label: `${nodeId} / child 3`,
216
+ icon: <FileText className="size-4 text-muted-foreground" />,
217
+ },
218
+ ];
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // Meta
223
+ // ---------------------------------------------------------------------------
224
+
225
+ const meta = {
226
+ title: "Navigation/Tree",
227
+ component: Tree,
228
+ tags: ["autodocs"],
229
+ args: {
230
+ nodes: fileTree,
231
+ defaultExpandedIds: ["src", "components"],
232
+ },
233
+ parameters: {
234
+ layout: "padded",
235
+ },
236
+ argTypes: {
237
+ nodes: {
238
+ description: "Array of TreeNode objects describing the tree structure.",
239
+ control: false,
240
+ table: { category: "Data" },
241
+ },
242
+ selectionMode: {
243
+ description: "Whether items can be selected — single, multiple, or none.",
244
+ control: { type: "radio" },
245
+ options: ["single", "multiple", "none"],
246
+ table: { category: "Behaviour" },
247
+ },
248
+ checkboxes: {
249
+ description: "Show checkboxes alongside nodes in multi-select mode.",
250
+ control: "boolean",
251
+ table: { category: "Behaviour" },
252
+ },
253
+ virtualize: {
254
+ description: "Enable windowed rendering for large trees (>50 visible rows).",
255
+ control: "boolean",
256
+ table: { category: "Behaviour" },
257
+ },
258
+ estimateRowHeight: {
259
+ description: "Estimated row height in px used by the virtualizer. Default 32.",
260
+ control: "number",
261
+ table: { category: "Behaviour" },
262
+ },
263
+ overscan: {
264
+ description: "Extra rows rendered beyond the visible window by the virtualizer. Default 8.",
265
+ control: "number",
266
+ table: { category: "Behaviour" },
267
+ },
268
+ maxHeight: {
269
+ description: "CSS max-height for the scroll viewport when virtualize is true.",
270
+ control: "text",
271
+ table: { category: "Behaviour" },
272
+ },
273
+ defaultExpandedIds: {
274
+ description: "Uncontrolled initial set of expanded node ids.",
275
+ control: false,
276
+ table: { category: "State" },
277
+ },
278
+ expandedIds: {
279
+ description: "Controlled set of expanded node ids.",
280
+ control: false,
281
+ table: { category: "State" },
282
+ },
283
+ onExpandedChange: {
284
+ description: "Callback when the expanded set changes.",
285
+ control: false,
286
+ table: { category: "Events" },
287
+ },
288
+ defaultSelectedIds: {
289
+ description: "Uncontrolled initial set of selected node ids.",
290
+ control: false,
291
+ table: { category: "State" },
292
+ },
293
+ selectedIds: {
294
+ description: "Controlled set of selected node ids.",
295
+ control: false,
296
+ table: { category: "State" },
297
+ },
298
+ onSelectionChange: {
299
+ description: "Callback when the selection set changes.",
300
+ control: false,
301
+ table: { category: "Events" },
302
+ },
303
+ loadChildren: {
304
+ description: "Async function to load children for a node with hasChildren: true.",
305
+ control: false,
306
+ table: { category: "Behaviour" },
307
+ },
308
+ className: {
309
+ description: "Additional CSS classes applied to the tree root.",
310
+ control: "text",
311
+ table: { category: "Styling" },
312
+ },
313
+ },
314
+ } satisfies Meta<typeof Tree>;
315
+
316
+ export default meta;
317
+ type Story = StoryObj<typeof meta>;
318
+
319
+ // ---------------------------------------------------------------------------
320
+ // Existing stories (unchanged)
321
+ // ---------------------------------------------------------------------------
322
+
323
+ /** Default single-selection file tree with icons. */
324
+ export const Default: Story = {
325
+ // Confirms the pre-expanded nodes are visible and clicking a node selects it.
326
+ play: async ({ canvasElement, userEvent }) => {
327
+ const canvas = within(canvasElement);
328
+ // "src" and "components" are pre-expanded via defaultExpandedIds
329
+ const srcItem = canvas.getByRole("treeitem", { name: /^src$/i });
330
+ await waitFor(() => expect(srcItem).toBeVisible());
331
+ const componentsItem = canvas.getByRole("treeitem", { name: /^components$/i });
332
+ await waitFor(() => expect(componentsItem).toBeVisible());
333
+ // Click a leaf node to select it
334
+ const buttonTsx = canvas.getByRole("treeitem", { name: /button\.tsx/i });
335
+ await userEvent.click(buttonTsx);
336
+ await expect(buttonTsx).toHaveAttribute("aria-selected", "true");
337
+ },
338
+ };
339
+
340
+ /** Multiple selection with checkboxes. */
341
+ export const Multiple: Story = {
342
+ args: {
343
+ nodes: orgTree,
344
+ defaultExpandedIds: ["engineering", "frontend", "backend", "data"],
345
+ selectionMode: "multiple",
346
+ checkboxes: true,
347
+ },
348
+ };
349
+
350
+ /** Fully expanded at mount via defaultExpandedIds. */
351
+ export const Expanded: Story = {
352
+ args: {
353
+ defaultExpandedIds: ["src", "components", "lib", "public"],
354
+ },
355
+ };
356
+
357
+ /** Collapsed — no nodes expanded at start. */
358
+ export const Collapsed: Story = {
359
+ args: {
360
+ defaultExpandedIds: [],
361
+ },
362
+ };
363
+
364
+ /** Nodes with disabled items. */
365
+ export const DisabledNodes: Story = {
366
+ args: {
367
+ nodes: orgTree,
368
+ defaultExpandedIds: ["engineering", "frontend", "backend", "data"],
369
+ selectionMode: "single",
370
+ },
371
+ };
372
+
373
+ /** Deep nesting — 4 levels. */
374
+ export const DeepNesting: Story = {
375
+ args: {
376
+ nodes: [
377
+ {
378
+ id: "l1",
379
+ label: "Level 1",
380
+ children: [
381
+ {
382
+ id: "l2",
383
+ label: "Level 2",
384
+ children: [
385
+ {
386
+ id: "l3",
387
+ label: "Level 3",
388
+ children: [
389
+ { id: "l4a", label: "Level 4 — A" },
390
+ { id: "l4b", label: "Level 4 — B" },
391
+ ],
392
+ },
393
+ ],
394
+ },
395
+ ],
396
+ },
397
+ ],
398
+ defaultExpandedIds: ["l1", "l2", "l3"],
399
+ },
400
+ };
401
+
402
+ /** Controlled expand + select with external state display. */
403
+ export const Controlled: Story = {
404
+ render: () => {
405
+ // eslint-disable-next-line react-hooks/rules-of-hooks
406
+ const [expanded, setExpanded] = useState<string[]>(["src"]);
407
+ // eslint-disable-next-line react-hooks/rules-of-hooks
408
+ const [selected, setSelected] = useState<string[]>([]);
409
+ return (
410
+ <div className="flex gap-8">
411
+ <Tree
412
+ nodes={fileTree}
413
+ expandedIds={expanded}
414
+ onExpandedChange={setExpanded}
415
+ selectedIds={selected}
416
+ onSelectionChange={setSelected}
417
+ className="w-64"
418
+ />
419
+ <div className="text-body text-muted-foreground space-y-1">
420
+ <p>
421
+ <span className="font-medium">Expanded:</span> {expanded.join(", ") || "none"}
422
+ </p>
423
+ <p>
424
+ <span className="font-medium">Selected:</span> {selected.join(", ") || "none"}
425
+ </p>
426
+ </div>
427
+ </div>
428
+ );
429
+ },
430
+ };
431
+
432
+ /** No selection mode — purely navigational. */
433
+ export const NoSelection: Story = {
434
+ args: {
435
+ selectionMode: "none",
436
+ defaultExpandedIds: ["src", "components"],
437
+ },
438
+ };
439
+
440
+ // ---------------------------------------------------------------------------
441
+ // New story: 5 000-node virtualized tree
442
+ // ---------------------------------------------------------------------------
443
+
444
+ /**
445
+ * 5 000 flat nodes rendered with `virtualize`. Only the visible window (~15–25
446
+ * rows) is mounted in the DOM at any time; keyboard navigation and End/Home
447
+ * work across all 5 000 items.
448
+ */
449
+ export const Virtualized: Story = {
450
+ args: {
451
+ nodes: LARGE_NODES,
452
+ virtualize: true,
453
+ estimateRowHeight: 32,
454
+ overscan: 8,
455
+ maxHeight: "400px",
456
+ selectionMode: "single",
457
+ defaultExpandedIds: [],
458
+ },
459
+ parameters: {
460
+ docs: {
461
+ description: {
462
+ story:
463
+ "5 000 flat nodes with `virtualize={true}`. Only the visible window is in the DOM. Use keyboard ArrowDown/Up/Home/End to navigate; the virtualizer scrolls automatically.",
464
+ },
465
+ },
466
+ },
467
+ };
468
+
469
+ /**
470
+ * `scrollToId` reveals a node the virtualizer has windowed out of the DOM. Setting
471
+ * `selectedIds` to a deep node alone would select it invisibly (structure shown,
472
+ * but not "where I am"); passing the same id as `scrollToId` centers it. Click a
473
+ * button — the matching row scrolls into view and shows as selected. (Use
474
+ * `scrollSelectionIntoView` to reveal on every selection change instead.)
475
+ */
476
+ function RevealSelectedNodeDemo() {
477
+ const [target, setTarget] = useState<string | undefined>(undefined);
478
+ return (
479
+ <div className="space-y-3">
480
+ <div className="flex flex-wrap gap-2">
481
+ {["node-50", "node-1200", "node-4300"].map((id) => (
482
+ <button
483
+ key={id}
484
+ type="button"
485
+ onClick={() => setTarget(id)}
486
+ className="rounded-md border border-border-strong bg-background px-3 py-1.5 text-body font-medium text-foreground hover:bg-surface-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
487
+ >
488
+ Navigate to Node {Number(id.split("-")[1]) + 1}
489
+ </button>
490
+ ))}
491
+ </div>
492
+ <Tree
493
+ nodes={LARGE_NODES}
494
+ virtualize
495
+ estimateRowHeight={32}
496
+ overscan={8}
497
+ maxHeight="400px"
498
+ selectionMode="single"
499
+ selectedIds={target ? [target] : []}
500
+ scrollToId={target}
501
+ />
502
+ </div>
503
+ );
504
+ }
505
+
506
+ export const RevealSelectedNode: Story = {
507
+ render: () => <RevealSelectedNodeDemo />,
508
+ parameters: {
509
+ docs: {
510
+ description: {
511
+ story:
512
+ "Reveal a programmatically-selected node in a 5 000-node virtualized tree. `scrollToId` scrolls the windowed-out row into view (and expands its lazy ancestors when needed); keyboard navigation is unaffected.",
513
+ },
514
+ },
515
+ },
516
+ play: async ({ canvasElement }) => {
517
+ const canvas = within(canvasElement);
518
+ // Node 4301 (id node-4300) is far below the fold — not mounted initially.
519
+ expect(canvas.queryByText("Node 4301")).toBeNull();
520
+ await userEvent.click(canvas.getByRole("button", { name: "Navigate to Node 4301" }));
521
+ // scrollToId scrolls the virtualizer to it, so its row mounts and centers.
522
+ await waitFor(() => expect(canvas.getByText("Node 4301")).toBeInTheDocument());
523
+ },
524
+ };
525
+
526
+ // ---------------------------------------------------------------------------
527
+ // New story: lazy-loading children on demand
528
+ // ---------------------------------------------------------------------------
529
+
530
+ /**
531
+ * `surface="sidebar"` renders the selected row with the sidebar's own active-item
532
+ * treatment (`bg-sidebar-accent` + `text-sidebar-accent-foreground` + `font-medium`),
533
+ * matching `SidebarMenuButton`'s active state — because the default `bg-accent`
534
+ * fill is near-invisible against `--sidebar`. No fill is ≥3:1 on a sidebar, so
535
+ * (like the Sidebar itself) perceivability comes from the foreground/weight shift
536
+ * plus `aria-selected`, not fill contrast alone.
537
+ *
538
+ * NOTE: cross-theme visual correctness (the three shipped themes — light,
539
+ * dark) requires a real render and is owed to a human reviewer;
540
+ * the tokens are semantic so they adapt, but pixel-level confirmation cannot be
541
+ * automated in this environment.
542
+ */
543
+ export const SidebarSurface: Story = {
544
+ args: {
545
+ nodes: fileTree,
546
+ surface: "sidebar",
547
+ defaultExpandedIds: ["src", "components"],
548
+ defaultSelectedIds: ["button"],
549
+ selectionMode: "single",
550
+ },
551
+ render: (args) => (
552
+ <div className="w-64 rounded-md bg-sidebar p-2">
553
+ <Tree {...args} />
554
+ </div>
555
+ ),
556
+ play: async ({ canvasElement }) => {
557
+ const canvas = within(canvasElement);
558
+ const selected = canvas.getByRole("treeitem", { name: /button\.tsx/i });
559
+ await waitFor(() => expect(selected).toBeVisible());
560
+ await expect(selected).toHaveAttribute("aria-selected", "true");
561
+ },
562
+ };
563
+
564
+ /**
565
+ * Nodes marked `hasChildren: true` with no loaded `children` show the expand
566
+ * chevron. On first expand, `loadChildren` is called; a skeleton loading row
567
+ * appears under `aria-busy` while the request is in flight. Resolved children
568
+ * are cached — re-expanding does not re-fetch. "Category C" simulates a
569
+ * network error and shows the inline retry affordance.
570
+ */
571
+ export const LazyLoading: Story = {
572
+ args: {
573
+ nodes: makeLazyRoot(),
574
+ loadChildren: (node) => fakeFetch(node.id),
575
+ selectionMode: "single",
576
+ defaultExpandedIds: [],
577
+ },
578
+ parameters: {
579
+ docs: {
580
+ description: {
581
+ story:
582
+ "Children are fetched on first expand via `loadChildren`. The spinner appears in the expand chevron while loading. A failed load shows a retry button.",
583
+ },
584
+ },
585
+ },
586
+ };
587
+
588
+ // ---------------------------------------------------------------------------
589
+ // New story: per-node trailing accessory (#369)
590
+ // ---------------------------------------------------------------------------
591
+
592
+ /**
593
+ * Per-node trailing content — e.g. a file-size badge — via `TreeNode.accessory`
594
+ * (#369). It renders as a SIBLING of the label, outside the row's interactive
595
+ * label region: it never joins the row's accessible name and clicking it never
596
+ * selects or expands the row.
597
+ */
598
+ export const TrailingAccessory: Story = {
599
+ args: {
600
+ nodes: fileTreeWithSizes,
601
+ defaultExpandedIds: ["src", "components"],
602
+ },
603
+ play: async ({ canvasElement, userEvent }) => {
604
+ const canvas = within(canvasElement);
605
+ const buttonRow = canvas.getByRole("treeitem", { name: "button.tsx" });
606
+ await waitFor(() => expect(buttonRow).toBeVisible());
607
+ // The accessible name is exactly the file name — the badge text never leaks in.
608
+ expect(canvas.queryByRole("treeitem", { name: /button\.tsx.*2\.1 KB/ })).toBeNull();
609
+
610
+ // Clicking the badge does not select the row.
611
+ await userEvent.click(canvas.getByText("2.1 KB"));
612
+ await expect(buttonRow).not.toHaveAttribute("aria-selected", "true");
613
+
614
+ // Clicking the row itself still selects it.
615
+ await userEvent.click(buttonRow);
616
+ await expect(buttonRow).toHaveAttribute("aria-selected", "true");
617
+ },
618
+ };