@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,132 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Ban, ShieldAlert } from "lucide-react";
3
+ import { STATUSES, StatusBadge, StatusIcon } from "./status-badge";
4
+
5
+ const meta = {
6
+ title: "Core/StatusBadge",
7
+ component: StatusBadge,
8
+ tags: ["autodocs"],
9
+ args: { status: "complete" },
10
+ argTypes: {
11
+ status: {
12
+ description:
13
+ "The canonical 7-state execution status (closed enum), or a mapped " +
14
+ "{label, tone, icon?} object for a domain the 7 don't cover (#363) — " +
15
+ "see the CustomVocabulary story. The control below drives the canonical form only.",
16
+ control: { type: "select" },
17
+ options: [
18
+ "pending",
19
+ "running",
20
+ "complete",
21
+ "awaiting-approval",
22
+ "denied",
23
+ "failed",
24
+ "skipped",
25
+ ],
26
+ table: { category: "State" },
27
+ },
28
+ size: {
29
+ description: "`md` for inline use; `sm` for rail nodes / dense rows.",
30
+ control: { type: "radio" },
31
+ options: ["sm", "md"],
32
+ table: { category: "Appearance" },
33
+ },
34
+ hideIcon: {
35
+ description: "Render label-only — omits the status icon.",
36
+ control: "boolean",
37
+ table: { category: "Appearance" },
38
+ },
39
+ children: {
40
+ description: "Override the default status label text.",
41
+ control: "text",
42
+ table: { category: "Content" },
43
+ },
44
+ className: {
45
+ description: "Extra Tailwind classes merged via cn().",
46
+ control: "text",
47
+ table: { category: "Appearance" },
48
+ },
49
+ },
50
+ } satisfies Meta<typeof StatusBadge>;
51
+ export default meta;
52
+ type Story = StoryObj<typeof meta>;
53
+
54
+ export const Default: Story = {};
55
+
56
+ /**
57
+ * The canonical 7-state vocabulary. Calm states (pending, running, complete,
58
+ * denied, skipped) use the quiet wash/neutral treatment; only the two
59
+ * attention states (awaiting-approval, failed) get the solid fill.
60
+ */
61
+ export const AllStatuses: Story = {
62
+ render: () => (
63
+ <div className="flex flex-wrap gap-2">
64
+ {STATUSES.map((status) => (
65
+ <StatusBadge key={status} status={status} />
66
+ ))}
67
+ </div>
68
+ ),
69
+ };
70
+
71
+ export const Sizes: Story = {
72
+ render: () => (
73
+ <div className="flex items-center gap-2">
74
+ <StatusBadge size="sm" status="running" />
75
+ <StatusBadge size="md" status="running" />
76
+ </div>
77
+ ),
78
+ };
79
+
80
+ /** `children` override the default label; counts are a typical use. */
81
+ export const CustomLabels: Story = {
82
+ render: () => (
83
+ <div className="flex flex-wrap gap-2">
84
+ <StatusBadge status="complete">12 passed</StatusBadge>
85
+ <StatusBadge status="failed">2 failed</StatusBadge>
86
+ <StatusBadge status="skipped">1 skipped</StatusBadge>
87
+ <StatusBadge hideIcon status="running">
88
+ Reconciling…
89
+ </StatusBadge>
90
+ </div>
91
+ ),
92
+ };
93
+
94
+ /**
95
+ * The bounded out-of-vocabulary escape hatch (#363): a run engine whose real
96
+ * states the 7 canonical values don't cover (`aborted`, `stopped_guardrail`, a
97
+ * distinct "not run" dash state) maps them to `{label, tone, icon?}` — CALM
98
+ * treatments only, so the calm/loud contrast against the canonical set below
99
+ * stays meaningful (only `awaiting-approval`/`failed` ever get a solid fill).
100
+ */
101
+ export const CustomVocabulary: Story = {
102
+ render: () => (
103
+ <div className="flex flex-wrap gap-2">
104
+ {STATUSES.map((status) => (
105
+ <StatusBadge key={status} status={status} />
106
+ ))}
107
+ <StatusBadge status={{ label: "Aborted", tone: "neutral" }} />
108
+ <StatusBadge status={{ label: "Stopped (guardrail)", tone: "warning", icon: ShieldAlert }} />
109
+ <StatusBadge
110
+ status={{ label: "Not run", tone: "neutral", icon: Ban }}
111
+ className="border-dashed"
112
+ />
113
+ </div>
114
+ ),
115
+ };
116
+
117
+ /**
118
+ * The `StatusIcon` atom for tight rows (test results, rail nodes) — the
119
+ * adjacent row text must carry the status name for AT users.
120
+ */
121
+ export const Icons: Story = {
122
+ render: () => (
123
+ <div className="flex flex-col gap-2">
124
+ {STATUSES.map((status) => (
125
+ <div key={status} className="flex items-center gap-2 text-body">
126
+ <StatusIcon status={status} />
127
+ <span>{status}</span>
128
+ </div>
129
+ ))}
130
+ </div>
131
+ ),
132
+ };
@@ -0,0 +1,189 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { ShieldAlert } from "lucide-react";
4
+ import {
5
+ STATUSES,
6
+ STATUS_LABELS,
7
+ StatusBadge,
8
+ StatusIcon,
9
+ fromTimelineStatus,
10
+ isCustomStatus,
11
+ type Status,
12
+ } from "./status-badge";
13
+
14
+ describe("StatusBadge", () => {
15
+ it("renders the default label for every canonical status", () => {
16
+ render(
17
+ <div>
18
+ {STATUSES.map((status) => (
19
+ <StatusBadge key={status} status={status} />
20
+ ))}
21
+ </div>,
22
+ );
23
+ for (const status of STATUSES) {
24
+ expect(screen.getByText(STATUS_LABELS[status])).toBeInTheDocument();
25
+ }
26
+ });
27
+
28
+ it("children override the default label", () => {
29
+ render(<StatusBadge status="complete">12 passed</StatusBadge>);
30
+ expect(screen.getByText("12 passed")).toBeInTheDocument();
31
+ expect(screen.queryByText("Complete")).not.toBeInTheDocument();
32
+ });
33
+
34
+ it("hides the decorative icon from AT and exposes data-status", () => {
35
+ const { container } = render(<StatusBadge status="failed" />);
36
+ const badge = container.querySelector('[data-status="failed"]');
37
+ expect(badge).not.toBeNull();
38
+ const icon = badge?.querySelector("svg");
39
+ expect(icon).not.toBeNull();
40
+ expect(icon?.getAttribute("aria-hidden")).toBe("true");
41
+ });
42
+
43
+ it("only the two attention states get the solid fill (hybrid visual)", () => {
44
+ const { container } = render(
45
+ <div>
46
+ {STATUSES.map((status) => (
47
+ <StatusBadge key={status} status={status} />
48
+ ))}
49
+ </div>,
50
+ );
51
+ const classesFor = (status: string) =>
52
+ container.querySelector(`[data-status="${status}"]`)?.className ?? "";
53
+ expect(classesFor("awaiting-approval")).toContain("bg-warning");
54
+ expect(classesFor("failed")).toContain("bg-destructive");
55
+ // calm states: wash or neutral, never the solid status fill
56
+ expect(classesFor("complete")).toContain("bg-success/10");
57
+ expect(classesFor("running")).toContain("bg-info/10");
58
+ expect(classesFor("pending")).toContain("bg-secondary");
59
+ });
60
+
61
+ it("running spins with a motion-reduce neutralizer", () => {
62
+ const { container } = render(<StatusBadge status="running" />);
63
+ const icon = container.querySelector("svg");
64
+ expect(icon?.getAttribute("class")).toContain("animate-spin");
65
+ expect(icon?.getAttribute("class")).toContain("motion-reduce:animate-none");
66
+ });
67
+
68
+ it("merges className last and spreads props", () => {
69
+ const { container } = render(
70
+ <StatusBadge className="bg-muted" data-testid="badge" status="pending" />,
71
+ );
72
+ const badge = container.querySelector('[data-testid="badge"]');
73
+ expect(badge?.className).toContain("bg-muted");
74
+ expect(badge?.className).not.toContain("bg-secondary");
75
+ });
76
+ });
77
+
78
+ describe("StatusIcon", () => {
79
+ it("is decorative (aria-hidden) and status-colored", () => {
80
+ const { container } = render(<StatusIcon status="complete" />);
81
+ const icon = container.querySelector("svg");
82
+ expect(icon?.getAttribute("aria-hidden")).toBe("true");
83
+ expect(icon?.getAttribute("class")).toContain("text-success-text");
84
+ });
85
+ });
86
+
87
+ describe("fromTimelineStatus", () => {
88
+ it("maps the timeline 3-state vocabulary losslessly", () => {
89
+ expect(fromTimelineStatus("done")).toBe("complete");
90
+ expect(fromTimelineStatus("active")).toBe("running");
91
+ expect(fromTimelineStatus("pending")).toBe("pending");
92
+ });
93
+ });
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Canonical class regression lock (#363) — the tone axis must never restyle a
97
+ // shipped calm/loud hybrid status.
98
+ // ---------------------------------------------------------------------------
99
+
100
+ describe("StatusBadge — canonical class regression lock (#363)", () => {
101
+ // Copied verbatim from statusBadgeVariants' `status` variant recipes.
102
+ const EXPECTED_STATUS_RECIPE: Record<Status, string> = {
103
+ pending: "border-transparent bg-secondary text-secondary-foreground",
104
+ running: "border-info/40 bg-info/10 text-info-text",
105
+ complete: "border-success/40 bg-success/10 text-success-text",
106
+ "awaiting-approval": "border-transparent bg-warning text-warning-foreground",
107
+ denied: "border-transparent bg-muted text-muted-foreground",
108
+ failed: "border-transparent bg-destructive text-destructive-foreground",
109
+ skipped: "border-transparent bg-secondary text-muted-foreground",
110
+ };
111
+
112
+ it("every canonical status still renders its exact, unchanged recipe classes", () => {
113
+ const { container } = render(
114
+ <div>
115
+ {STATUSES.map((status) => (
116
+ <StatusBadge key={status} status={status} />
117
+ ))}
118
+ </div>,
119
+ );
120
+ for (const status of STATUSES) {
121
+ const badge = container.querySelector(`[data-status="${status}"]`);
122
+ expect(badge).not.toBeNull();
123
+ for (const cls of EXPECTED_STATUS_RECIPE[status].split(" ")) {
124
+ expect(badge?.classList.contains(cls)).toBe(true);
125
+ }
126
+ }
127
+ });
128
+ });
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Custom-status escape hatch (#363)
132
+ // ---------------------------------------------------------------------------
133
+
134
+ describe("StatusBadge — custom status escape hatch (#363)", () => {
135
+ it("renders a custom {label, tone, icon} status with the calm tone wash, not a canonical fill", () => {
136
+ render(
137
+ <StatusBadge status={{ label: "Stopped (guardrail)", tone: "warning", icon: ShieldAlert }} />,
138
+ );
139
+ const badge = screen.getByText("Stopped (guardrail)").closest("[data-slot='status-badge']");
140
+ expect(badge).not.toBeNull();
141
+ expect(badge).toHaveAttribute("data-status", "custom");
142
+ expect(badge).toHaveAttribute("data-tone", "warning");
143
+ // calm alpha-wash, matching the canonical `running`/`complete` recipe shape
144
+ expect(badge?.classList.contains("bg-warning/10")).toBe(true);
145
+ expect(badge?.classList.contains("text-warning-text")).toBe(true);
146
+ // NOT the solid `awaiting-approval` attention fill
147
+ expect(badge?.classList.contains("bg-warning")).toBe(false);
148
+ // the icon rendered and is decorative
149
+ const icon = badge?.querySelector("svg");
150
+ expect(icon).not.toBeNull();
151
+ expect(icon?.getAttribute("aria-hidden")).toBe("true");
152
+ });
153
+
154
+ it("a custom status with no icon renders label-only", () => {
155
+ render(<StatusBadge status={{ label: "Aborted", tone: "neutral" }} />);
156
+ const badge = screen.getByText("Aborted").closest("[data-slot='status-badge']");
157
+ expect(badge?.querySelector("svg")).toBeNull();
158
+ });
159
+
160
+ it("hideIcon suppresses the icon on a custom status that has one", () => {
161
+ render(
162
+ <StatusBadge status={{ label: "Stopped", tone: "warning", icon: ShieldAlert }} hideIcon />,
163
+ );
164
+ const badge = screen.getByText("Stopped").closest("[data-slot='status-badge']");
165
+ expect(badge?.querySelector("svg")).toBeNull();
166
+ });
167
+
168
+ it("children override a custom status's label too", () => {
169
+ render(<StatusBadge status={{ label: "Aborted", tone: "neutral" }}>Custom text</StatusBadge>);
170
+ expect(screen.getByText("Custom text")).toBeInTheDocument();
171
+ expect(screen.queryByText("Aborted")).not.toBeInTheDocument();
172
+ });
173
+
174
+ it("isCustomStatus narrows correctly for both branches (runtime)", () => {
175
+ expect(isCustomStatus("complete")).toBe(false);
176
+ expect(isCustomStatus({ label: "Aborted", tone: "neutral" })).toBe(true);
177
+ });
178
+
179
+ it("TYPE LOCK: a canonical status cannot also carry a tone prop (integrity constraint 1)", () => {
180
+ // `tone` is reachable ONLY through the CustomStatus object form — never as
181
+ // an independent prop — so a canonical status can never be recolored.
182
+ const typeOnlyRecolorAttempt = () => (
183
+ // @ts-expect-error — `tone` is not a public StatusBadge prop; passing it
184
+ // alongside a canonical `status` string must fail to compile (#363).
185
+ <StatusBadge status="failed" tone="success" />
186
+ );
187
+ expect(typeof typeOnlyRecolorAttempt).toBe("function");
188
+ });
189
+ });
@@ -0,0 +1,330 @@
1
+ /**
2
+ * StatusBadge — the canonical execution-status vocabulary (#189, research 10 §B.1).
3
+ *
4
+ * One CLOSED 7-state enum unifies the five status idioms across the agent
5
+ * trace components (`@elabs-ai/components-ai` Tool / ChainOfThought / Confirmation /
6
+ * TestResults / Sandbox), so one agent run reads as one author and an agent
7
+ * picks a status by MEANING — it cannot emit a color or a freeform string.
8
+ *
9
+ * Hybrid visual (research 08 "green deliberate / status sparingly"): the calm
10
+ * states — including `complete` — render the quiet `bg-<status>/10` alpha-wash
11
+ * or a neutral fill; ONLY the two attention states (`awaiting-approval`,
12
+ * `failed`) get the solid status fill, so a settled transcript stays calm and
13
+ * the states that need a human grab the eye.
14
+ *
15
+ * Builds ON the existing Badge cva (badge.tsx) — same pill geometry and
16
+ * typography; status colors are semantic tokens only.
17
+ *
18
+ * BOUNDED ESCAPE HATCH (#363): a domain that genuinely has a state the 7
19
+ * canonical values don't express (e.g. a run engine's `stopped_guardrail`) can
20
+ * pass a `CustomStatus` object instead of a `Status` string. Three integrity
21
+ * constraints keep it from eroding the anti-hallucination property above:
22
+ *
23
+ * 1. `tone` is reachable ONLY through the object form (which requires a
24
+ * `label`) — it is NOT an independent prop, so a canonical status can
25
+ * never be recolored (`status="failed" tone="success"` is unrepresentable).
26
+ * 2. The hatch is CALM-ONLY: all five tones render the quiet alpha-wash/
27
+ * neutral treatments. The solid attention fill stays exclusive to the
28
+ * canonical `awaiting-approval` and `failed` — no consumer can mint a
29
+ * loud chip, or the loud/calm signal degrades to noise.
30
+ * 3. No raw color, ever: `tone` maps to existing semantic tokens only
31
+ * (secondary / info / success / warning / destructive).
32
+ *
33
+ * Map a custom status ONCE, near your domain — not per call site. `StatusIcon`
34
+ * stays canonical-only (the enum's own icon+color atom); there is no `emphasis`/
35
+ * `loud` option on the hatch; `fromTimelineStatus()` is unchanged.
36
+ */
37
+ import { forwardRef, type HTMLAttributes } from "react";
38
+ import { cva, type VariantProps } from "class-variance-authority";
39
+ import {
40
+ AlertCircle,
41
+ CheckCircle2,
42
+ Circle,
43
+ Clock,
44
+ Loader2,
45
+ MinusCircle,
46
+ XCircle,
47
+ type LucideIcon,
48
+ type LucideProps,
49
+ } from "lucide-react";
50
+ import { badgeVariants } from "../badge/badge";
51
+ import { cn } from "../../lib/cn";
52
+
53
+ /** The canonical, closed 7-state status enum (research 10 §B.1). */
54
+ export const STATUSES = [
55
+ "pending",
56
+ "running",
57
+ "complete",
58
+ "awaiting-approval",
59
+ "denied",
60
+ "failed",
61
+ "skipped",
62
+ ] as const;
63
+
64
+ export type Status = (typeof STATUSES)[number];
65
+
66
+ /** Closed tone set for the out-of-vocabulary escape hatch (CALM treatments only). */
67
+ export const STATUS_TONES = ["neutral", "info", "success", "warning", "destructive"] as const;
68
+
69
+ export type StatusTone = (typeof STATUS_TONES)[number];
70
+
71
+ /**
72
+ * An out-of-vocabulary status. Prefer the canonical `Status` enum; reach for
73
+ * this only when the domain genuinely has a state the 7 do not express (e.g. a
74
+ * run engine's `stopped_guardrail`). Map it ONCE, near your domain — not per
75
+ * call site.
76
+ */
77
+ export interface CustomStatus {
78
+ label: string;
79
+ tone: StatusTone;
80
+ /** Optional Lucide glyph. Omitted → label-only (no default glyph). */
81
+ icon?: LucideIcon;
82
+ }
83
+
84
+ /** Narrows `Status | CustomStatus` — the object form is always the escape hatch. */
85
+ export function isCustomStatus(status: Status | CustomStatus): status is CustomStatus {
86
+ return typeof status === "object" && status !== null;
87
+ }
88
+
89
+ /**
90
+ * The tone each CANONICAL status maps to, for the informational `data-tone`
91
+ * attribute only — canonical rendering always goes through the untouched
92
+ * `status` cva branch below, never through `tone`.
93
+ */
94
+ const STATUS_TONE_MAP: Record<Status, StatusTone> = {
95
+ pending: "neutral",
96
+ running: "info",
97
+ complete: "success",
98
+ "awaiting-approval": "warning",
99
+ denied: "neutral",
100
+ failed: "destructive",
101
+ skipped: "neutral",
102
+ };
103
+
104
+ /**
105
+ * The canonical semantic ROLE per status — the single source of truth for
106
+ * status colour (#392). Any other status-keyed colour map in the repo
107
+ * (`Timeline`'s `NODE_STYLE`, or a future one) must be TESTED against this,
108
+ * not hand-authored — a second literal copy is exactly how `NODE_STYLE`
109
+ * drifted (`running` rendered `--primary` while this map said `--info`).
110
+ * Do NOT interpolate `bg-${STATUS_ROLE[status]}` into a class map: Tailwind
111
+ * scans source statically, so a template-literal class emits no CSS. Keep
112
+ * consuming maps as literal class strings, verified against this export by a
113
+ * test.
114
+ */
115
+ export const STATUS_ROLE = {
116
+ pending: "secondary",
117
+ running: "info",
118
+ complete: "success",
119
+ "awaiting-approval": "warning",
120
+ denied: "muted",
121
+ failed: "destructive",
122
+ skipped: "secondary",
123
+ } as const satisfies Record<Status, string>;
124
+
125
+ /** Default human-readable label per status (override via `children`). */
126
+ export const STATUS_LABELS: Record<Status, string> = {
127
+ pending: "Pending",
128
+ running: "Running",
129
+ complete: "Complete",
130
+ "awaiting-approval": "Awaiting approval",
131
+ denied: "Denied",
132
+ failed: "Failed",
133
+ skipped: "Skipped",
134
+ };
135
+
136
+ const STATUS_ICONS: Record<Status, LucideIcon> = {
137
+ pending: Circle,
138
+ running: Loader2,
139
+ complete: CheckCircle2,
140
+ "awaiting-approval": Clock,
141
+ denied: XCircle,
142
+ failed: AlertCircle,
143
+ skipped: MinusCircle,
144
+ };
145
+
146
+ /**
147
+ * Lucide glyph per out-of-vocabulary TONE — the same five-tone escape hatch
148
+ * `CustomStatus` uses (`STATUS_TONES`). A consumer that encodes a bare TONE
149
+ * with no fuller `Status` semantics (e.g. `@elabs-ai/components-flow`'s `FlowNode.tone`,
150
+ * #387) derives its non-colour cue from here instead of inventing a second
151
+ * icon vocabulary — each entry reuses the SAME icon `STATUS_ICONS` already
152
+ * pairs with that tone's one canonical status (`success`→`complete`'s
153
+ * `CheckCircle2`, `warning`→`awaiting-approval`'s `Clock`,
154
+ * `destructive`→`failed`'s `AlertCircle`). `neutral`/`info` have no entry:
155
+ * nothing consumes them yet, and reaching for a glyph on every neutral/
156
+ * default-toned element would be assistive-tech noise on the common case —
157
+ * extend this map, don't fork a new one, the day something needs them.
158
+ */
159
+ export const STATUS_TONE_ICONS: Partial<Record<StatusTone, LucideIcon>> = {
160
+ success: CheckCircle2,
161
+ warning: Clock,
162
+ destructive: AlertCircle,
163
+ };
164
+
165
+ /**
166
+ * On-surface icon color per status for the standalone `StatusIcon` atom.
167
+ * (Inside `StatusBadge` the icon inherits the badge foreground instead, so it
168
+ * stays legible on the solid attention fills.)
169
+ */
170
+ const STATUS_ICON_CLASSES: Record<Status, string> = {
171
+ pending: "text-muted-foreground",
172
+ running: "text-info-text",
173
+ complete: "text-success-text",
174
+ "awaiting-approval": "text-warning-text",
175
+ denied: "text-muted-foreground",
176
+ failed: "text-destructive-text",
177
+ skipped: "text-muted-foreground",
178
+ };
179
+
180
+ /** `animate-spin` is a continuous loop (ease-linear is the allowed loop ease). */
181
+ const RUNNING_SPIN_CLASSES = "animate-spin motion-reduce:animate-none";
182
+
183
+ export interface StatusIconProps extends Omit<LucideProps, "ref"> {
184
+ /** The canonical execution status (closed enum). */
185
+ status: Status;
186
+ }
187
+
188
+ /**
189
+ * The icon + status-color atom `StatusBadge` itself composes — for tight rows
190
+ * (test-result rows, timeline/rail nodes) where a full badge is too heavy.
191
+ * Decorative by default (`aria-hidden`); the adjacent row text must carry the
192
+ * status name for AT users.
193
+ */
194
+ export const StatusIcon = forwardRef<SVGSVGElement, StatusIconProps>(function StatusIcon(
195
+ { status, className, ...props },
196
+ ref,
197
+ ) {
198
+ const Icon = STATUS_ICONS[status];
199
+ return (
200
+ <Icon
201
+ ref={ref}
202
+ aria-hidden="true"
203
+ data-status={status}
204
+ className={cn(
205
+ "size-4 shrink-0",
206
+ STATUS_ICON_CLASSES[status],
207
+ status === "running" && RUNNING_SPIN_CLASSES,
208
+ className,
209
+ )}
210
+ {...props}
211
+ />
212
+ );
213
+ });
214
+
215
+ export const statusBadgeVariants = cva(
216
+ // Builds on the Badge pill (geometry + typography from badge.tsx). The
217
+ // `outline` variant carries no fill, so each status sets its own
218
+ // border/bg/text — conflicts resolve via cn() (later utilities win).
219
+ cn(badgeVariants({ variant: "outline" }), "[&_svg]:shrink-0"),
220
+ {
221
+ variants: {
222
+ status: {
223
+ // calm: neutral fill
224
+ pending: "border-transparent bg-secondary text-secondary-foreground",
225
+ // calm: status alpha-wash (escapes the decoration drawn-not-filled rule)
226
+ running: "border-info/40 bg-info/10 text-info-text",
227
+ complete: "border-success/40 bg-success/10 text-success-text",
228
+ // ATTENTION: solid fill — must grab the eye
229
+ "awaiting-approval": "border-transparent bg-warning text-warning-foreground",
230
+ // calm: de-emphasized neutrals
231
+ denied: "border-transparent bg-muted text-muted-foreground",
232
+ // ATTENTION: solid fill — must grab the eye
233
+ failed: "border-transparent bg-destructive text-destructive-foreground",
234
+ skipped: "border-transparent bg-secondary text-muted-foreground",
235
+ },
236
+ /**
237
+ * The out-of-vocabulary escape hatch (#363) — CALM treatments only (the
238
+ * same alpha-wash/neutral technique as the calm `status` recipes above).
239
+ * There is deliberately no solid/attention recipe here: the loud fill
240
+ * stays exclusive to the canonical `awaiting-approval`/`failed` states
241
+ * (integrity constraint 2, see the file docblock).
242
+ */
243
+ tone: {
244
+ neutral: "border-transparent bg-secondary text-secondary-foreground",
245
+ info: "border-info/40 bg-info/10 text-info-text",
246
+ success: "border-success/40 bg-success/10 text-success-text",
247
+ warning: "border-warning/40 bg-warning/10 text-warning-text",
248
+ destructive: "border-destructive/40 bg-destructive/10 text-destructive-text",
249
+ },
250
+ /** `md` for inline use; `sm` for rail nodes / dense rows. */
251
+ size: {
252
+ sm: "px-2 [&_svg]:size-3",
253
+ md: "[&_svg]:size-3.5",
254
+ },
255
+ },
256
+ defaultVariants: { size: "md" },
257
+ },
258
+ );
259
+
260
+ export interface StatusBadgeProps
261
+ extends
262
+ HTMLAttributes<HTMLSpanElement>,
263
+ Omit<VariantProps<typeof statusBadgeVariants>, "status" | "tone"> {
264
+ /** The canonical execution status, or a mapped out-of-vocabulary one (#363). */
265
+ status: Status | CustomStatus;
266
+ /** Render label-only (no status icon). */
267
+ hideIcon?: boolean;
268
+ }
269
+
270
+ export const StatusBadge = forwardRef<HTMLSpanElement, StatusBadgeProps>(function StatusBadge(
271
+ { status, size, hideIcon = false, className, children, ...props },
272
+ ref,
273
+ ) {
274
+ if (isCustomStatus(status)) {
275
+ const { label, tone, icon: Icon } = status;
276
+ return (
277
+ <span
278
+ ref={ref}
279
+ data-slot="status-badge"
280
+ data-status="custom"
281
+ data-tone={tone}
282
+ className={cn(statusBadgeVariants({ tone, size }), className)}
283
+ {...props}
284
+ >
285
+ {hideIcon || !Icon ? null : <Icon aria-hidden="true" />}
286
+ {children ?? label}
287
+ </span>
288
+ );
289
+ }
290
+
291
+ const Icon = STATUS_ICONS[status];
292
+ return (
293
+ <span
294
+ ref={ref}
295
+ data-slot="status-badge"
296
+ data-status={status}
297
+ data-tone={STATUS_TONE_MAP[status]}
298
+ className={cn(statusBadgeVariants({ status, size }), className)}
299
+ {...props}
300
+ >
301
+ {hideIcon ? null : (
302
+ <Icon
303
+ aria-hidden="true"
304
+ className={status === "running" ? RUNNING_SPIN_CLASSES : undefined}
305
+ />
306
+ )}
307
+ {children ?? STATUS_LABELS[status]}
308
+ </span>
309
+ );
310
+ });
311
+
312
+ /** The editor/markdown `Timeline` 3-state vocabulary (`@elabs-ai/components-editor`). */
313
+ export type TimelineStatus = "done" | "active" | "pending";
314
+
315
+ const TIMELINE_STATUS_MAP: Record<TimelineStatus, Status> = {
316
+ done: "complete",
317
+ active: "running",
318
+ pending: "pending",
319
+ };
320
+
321
+ /**
322
+ * Map the Timeline `done|active|pending` vocabulary onto the canonical
323
+ * `Status` (research 10 §B.1 mapping b — lossless; the timeline only
324
+ * expresses 3 of the 7 states). The AI-SDK `ToolUIPart` mapper lives in
325
+ * `@elabs-ai/components-ai` (`statusFromToolState`, tool.tsx) so `@elabs-ai/components-ui` stays free of
326
+ * the `ai` dependency (D6).
327
+ */
328
+ export function fromTimelineStatus(status: TimelineStatus): Status {
329
+ return TIMELINE_STATUS_MAP[status];
330
+ }
@@ -0,0 +1 @@
1
+ export { Switch } from "./switch";