@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,27 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Spinner } from "./spinner";
3
+ const meta = {
4
+ title: "Display/Spinner",
5
+ component: Spinner,
6
+ tags: ["autodocs"],
7
+ argTypes: {
8
+ label: {
9
+ description:
10
+ 'Accessible label read by screen readers (role="status" + aria-label). Default: "Loading".',
11
+ control: "text",
12
+ table: { category: "Content" },
13
+ },
14
+ className: {
15
+ description: "Extra Tailwind classes merged via cn() — use to control size and color.",
16
+ control: "text",
17
+ table: { category: "Appearance" },
18
+ },
19
+ },
20
+ } satisfies Meta<typeof Spinner>;
21
+ export default meta;
22
+ type Story = StoryObj<typeof meta>;
23
+ export const Default: Story = { render: () => <Spinner className="size-6" /> };
24
+ export const Large: Story = { render: () => <Spinner className="size-10" /> };
25
+ export const CustomLabel: Story = {
26
+ render: () => <Spinner className="size-6" label="Processing…" />,
27
+ };
@@ -0,0 +1,19 @@
1
+ import { Loader2 } from "lucide-react";
2
+ import { cn } from "../../lib/cn";
3
+
4
+ export interface SpinnerProps extends React.SVGProps<SVGSVGElement> {
5
+ /** Accessible label; defaults to "Loading". */
6
+ label?: string;
7
+ }
8
+
9
+ /** Indeterminate spinner with an accessible label. */
10
+ export function Spinner({ className, label = "Loading", ...props }: SpinnerProps) {
11
+ return (
12
+ <Loader2
13
+ role="status"
14
+ aria-label={label}
15
+ className={cn("size-4 animate-spin text-muted-foreground", className)}
16
+ {...props}
17
+ />
18
+ );
19
+ }
@@ -0,0 +1 @@
1
+ export { SplitPanel, type SplitPanelProps, type SplitPanelTone } from "./split-panel";
@@ -0,0 +1,60 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { SplitPanel } from "./split-panel";
3
+
4
+ const meta = {
5
+ title: "Layout/SplitPanel",
6
+ component: SplitPanel,
7
+ parameters: { layout: "fullscreen" },
8
+ } satisfies Meta<typeof SplitPanel>;
9
+ export default meta;
10
+ type Story = StoryObj<typeof meta>;
11
+
12
+ export const MasterDetail: Story = {
13
+ render: () => (
14
+ <div className="h-[400px]">
15
+ <SplitPanel
16
+ startSize="280px"
17
+ start={<div className="space-y-2 p-4 text-body">List pane</div>}
18
+ end={<div className="p-6 text-body text-muted-foreground">Detail pane</div>}
19
+ />
20
+ </div>
21
+ ),
22
+ };
23
+
24
+ /**
25
+ * Ground-offset tiering: the detail pane is RAISED as a white card (`endTone="card"`)
26
+ * on the page/list ground, so it reads as a distinct surface in every theme without a
27
+ * heavier divider — a card is lighter than the ground in both light and dark (in light
28
+ * the page itself is recessed below white; see research/structural-design 08 §H). A
29
+ * `muted` list well is a light-theme-only enhancement (in dark, `--surface-muted` reads
30
+ * lighter than `--card`), so this demo keeps the list `plain` to read correctly across
31
+ * all three themes.
32
+ */
33
+ export const Tiered: Story = {
34
+ render: () => (
35
+ <div className="h-[400px] bg-background p-4">
36
+ <SplitPanel
37
+ startSize="280px"
38
+ endTone="card"
39
+ divider={false}
40
+ className="rounded-lg"
41
+ endClassName="rounded-e-lg"
42
+ start={
43
+ <div className="space-y-2 p-4 text-body">
44
+ <p className="font-medium">Produced assets</p>
45
+ <p className="text-muted-foreground">board-note.md</p>
46
+ <p className="text-muted-foreground">q3-vs-q2.csv</p>
47
+ </div>
48
+ }
49
+ end={
50
+ <div className="space-y-3 p-6 text-body">
51
+ <p className="font-medium">board-note.md</p>
52
+ <p className="text-muted-foreground">
53
+ The raised detail card lifts off the list/page ground by surface offset.
54
+ </p>
55
+ </div>
56
+ }
57
+ />
58
+ </div>
59
+ ),
60
+ };
@@ -0,0 +1,38 @@
1
+ import { render } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { SplitPanel } from "./split-panel";
4
+
5
+ describe("SplitPanel", () => {
6
+ it("renders both panes", () => {
7
+ const { getByText } = render(<SplitPanel start={<>List</>} end={<>Detail</>} />);
8
+ expect(getByText("List")).toBeInTheDocument();
9
+ expect(getByText("Detail")).toBeInTheDocument();
10
+ });
11
+
12
+ // The pane content is bare text, so getByText returns the pane <div> itself.
13
+ it("defaults to plain tone (no ground offset) and a divider on the end pane", () => {
14
+ const { getByText } = render(<SplitPanel start={<>List</>} end={<>Detail</>} />);
15
+ const startPane = getByText("List");
16
+ const endPane = getByText("Detail");
17
+ expect(startPane.className).not.toMatch(/bg-surface-muted|bg-card/);
18
+ expect(endPane.className).not.toMatch(/bg-surface-muted|bg-card/);
19
+ expect(endPane.className).toMatch(/border-s/);
20
+ });
21
+
22
+ it("applies the ground-offset tones: muted well + raised card", () => {
23
+ const { getByText } = render(
24
+ <SplitPanel
25
+ startTone="muted"
26
+ endTone="card"
27
+ divider={false}
28
+ start={<>List</>}
29
+ end={<>Detail</>}
30
+ />,
31
+ );
32
+ expect(getByText("List").className).toMatch(/bg-surface-muted/);
33
+ const endPane = getByText("Detail");
34
+ expect(endPane.className).toMatch(/bg-card/);
35
+ expect(endPane.className).toMatch(/shadow-sm/);
36
+ expect(endPane.className).not.toMatch(/border-s/);
37
+ });
38
+ });
@@ -0,0 +1,95 @@
1
+ import { type ReactNode } from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "../../lib/cn";
4
+
5
+ /**
6
+ * Per-pane surface tone — the ground-offset tiering axis (research 08 §H, #193).
7
+ * `plain` keeps the ambient ground (default; non-breaking). `card` RAISES the pane as
8
+ * a white `bg-card` + `shadow-sm` and is robust across all three themes — a card is
9
+ * lighter than the page ground in BOTH light and dark, so it always reads as raised.
10
+ * `muted` (`bg-surface-muted`) reads as a recessed well ONLY in light themes; in dark
11
+ * themes `--surface-muted` is lighter than `--card`, so a `muted` pane reads as a faint
12
+ * raised tint, not a recess (dark-mode layering only goes UP — there is no surface below
13
+ * the page ground). For a guaranteed cross-theme raise/recess contrast, raise the focus
14
+ * pane with `card` and leave the other `plain`.
15
+ */
16
+ const paneToneVariants = cva("min-h-0 min-w-0 overflow-auto", {
17
+ variants: {
18
+ tone: {
19
+ plain: "",
20
+ muted: "bg-surface-muted",
21
+ card: "bg-card shadow-sm",
22
+ },
23
+ },
24
+ defaultVariants: { tone: "plain" },
25
+ });
26
+
27
+ export type SplitPanelTone = NonNullable<VariantProps<typeof paneToneVariants>["tone"]>;
28
+
29
+ export interface SplitPanelProps {
30
+ start: ReactNode;
31
+ end: ReactNode;
32
+ /** Layout axis. Defaults to "horizontal" (side-by-side). */
33
+ direction?: "horizontal" | "vertical";
34
+ /**
35
+ * Size of the START pane as a CSS grid track (e.g. "320px", "40%", "1fr").
36
+ * The END pane fills the remainder. Defaults to "1fr" (equal split).
37
+ */
38
+ startSize?: string;
39
+ /** Show a thin divider between panes. Defaults to true. */
40
+ divider?: boolean;
41
+ /** Surface tone of the START pane (ground-offset tiering). Defaults to "plain". */
42
+ startTone?: SplitPanelTone;
43
+ /** Surface tone of the END pane (ground-offset tiering). Defaults to "plain". */
44
+ endTone?: SplitPanelTone;
45
+ className?: string;
46
+ startClassName?: string;
47
+ endClassName?: string;
48
+ }
49
+
50
+ /**
51
+ * Two-pane layout for master/detail, editor/preview and inspector workflows.
52
+ * Static sizing by design (predictable in tests/SSR); for drag-to-resize wrap
53
+ * with a resize hook or compose `react-resizable-panels` at the app level.
54
+ *
55
+ * For a master/detail surface where the detail should read as a raised card on a
56
+ * recessed list, use `startTone="muted" endTone="card"` (ground-offset tiering).
57
+ */
58
+ export function SplitPanel({
59
+ start,
60
+ end,
61
+ direction = "horizontal",
62
+ startSize = "1fr",
63
+ divider = true,
64
+ startTone = "plain",
65
+ endTone = "plain",
66
+ className,
67
+ startClassName,
68
+ endClassName,
69
+ }: SplitPanelProps) {
70
+ const isHorizontal = direction === "horizontal";
71
+ const style = isHorizontal
72
+ ? { gridTemplateColumns: `${startSize} minmax(0, 1fr)` }
73
+ : { gridTemplateRows: `${startSize} minmax(0, 1fr)` };
74
+ return (
75
+ <div
76
+ className={cn(
77
+ "grid h-full w-full",
78
+ isHorizontal ? "grid-flow-col" : "grid-flow-row",
79
+ className,
80
+ )}
81
+ style={style}
82
+ >
83
+ <div className={cn(paneToneVariants({ tone: startTone }), startClassName)}>{start}</div>
84
+ <div
85
+ className={cn(
86
+ paneToneVariants({ tone: endTone }),
87
+ divider && (isHorizontal ? "border-s" : "border-t"),
88
+ endClassName,
89
+ )}
90
+ >
91
+ {end}
92
+ </div>
93
+ </div>
94
+ );
95
+ }
@@ -0,0 +1 @@
1
+ export { StatePanel, statePanelVariants, type StatePanelProps } from "./state-panel";
@@ -0,0 +1,80 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Button } from "../button";
3
+ import { StatePanel } from "./state-panel";
4
+
5
+ const meta = {
6
+ title: "States/StatePanel",
7
+ component: StatePanel,
8
+ tags: ["autodocs"],
9
+ argTypes: {
10
+ kind: {
11
+ control: "select",
12
+ options: ["empty", "error", "loading"],
13
+ },
14
+ size: {
15
+ control: "select",
16
+ options: ["sm", "md", "lg"],
17
+ },
18
+ },
19
+ } satisfies Meta<typeof StatePanel>;
20
+ export default meta;
21
+ type Story = StoryObj<typeof meta>;
22
+
23
+ export const Empty: Story = {
24
+ args: {
25
+ kind: "empty",
26
+ title: "No projects yet",
27
+ description: "Create your first project to get started.",
28
+ actions: <Button size="sm">New project</Button>,
29
+ },
30
+ };
31
+
32
+ export const Error: Story = {
33
+ args: {
34
+ kind: "error",
35
+ actions: (
36
+ <Button size="sm" variant="outline">
37
+ Retry
38
+ </Button>
39
+ ),
40
+ },
41
+ };
42
+
43
+ export const ErrorCustom: Story = {
44
+ name: "Error (custom title)",
45
+ args: {
46
+ kind: "error",
47
+ title: "Failed to load data",
48
+ description: "The server returned an unexpected response.",
49
+ actions: (
50
+ <Button size="sm" variant="outline">
51
+ Try again
52
+ </Button>
53
+ ),
54
+ },
55
+ };
56
+
57
+ export const Loading: Story = {
58
+ args: {
59
+ kind: "loading",
60
+ loadingLabel: "Loading workspace…",
61
+ },
62
+ };
63
+
64
+ export const LoadingLarge: Story = {
65
+ name: "Loading (large spinner)",
66
+ args: {
67
+ kind: "loading",
68
+ size: "lg",
69
+ loadingLabel: "Fetching results…",
70
+ },
71
+ };
72
+
73
+ export const EmptyNoIcon: Story = {
74
+ name: "Empty (no icon, no actions)",
75
+ args: {
76
+ kind: "empty",
77
+ title: "Nothing here",
78
+ description: "Items will appear here once available.",
79
+ },
80
+ };
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { StatePanel } from "./state-panel";
4
+
5
+ describe("StatePanel", () => {
6
+ it("renders empty kind with title and description", () => {
7
+ render(<StatePanel kind="empty" title="No items" description="Nothing here yet" />);
8
+ expect(screen.getByText("No items")).toBeInTheDocument();
9
+ expect(screen.getByText("Nothing here yet")).toBeInTheDocument();
10
+ // No ARIA role on empty panels
11
+ expect(screen.queryByRole("status")).toBeNull();
12
+ expect(screen.queryByRole("alert")).toBeNull();
13
+ });
14
+
15
+ it("renders error kind with role=alert, structural eyebrow, and default copy", () => {
16
+ render(<StatePanel kind="error" />);
17
+ const alert = screen.getByRole("alert");
18
+ expect(alert).toBeInTheDocument();
19
+ // Structural eyebrow label — non-color cue for monochrome themes (#179).
20
+ expect(screen.getByText("Error")).toBeInTheDocument();
21
+ expect(screen.getByText("Something went wrong")).toBeInTheDocument();
22
+ expect(screen.getByText("An unexpected error occurred. Please try again.")).toBeInTheDocument();
23
+ // data-kind attribute hook for future CSS targeting.
24
+ expect(alert).toHaveAttribute("data-kind", "error");
25
+ });
26
+
27
+ it("renders error kind with custom title", () => {
28
+ render(<StatePanel kind="error" title="Custom error" description="Custom description" />);
29
+ expect(screen.getByRole("alert")).toBeInTheDocument();
30
+ expect(screen.getByText("Custom error")).toBeInTheDocument();
31
+ expect(screen.getByText("Custom description")).toBeInTheDocument();
32
+ });
33
+
34
+ it("renders loading kind with role=status and live region", () => {
35
+ render(<StatePanel kind="loading" loadingLabel="Loading data…" />);
36
+ const status = screen.getByRole("status");
37
+ expect(status).toBeInTheDocument();
38
+ expect(status).toHaveAttribute("aria-live", "polite");
39
+ expect(screen.getByText("Loading data…")).toBeInTheDocument();
40
+ });
41
+
42
+ it("renders loading kind with default label", () => {
43
+ render(<StatePanel kind="loading" />);
44
+ expect(screen.getByText("Loading…")).toBeInTheDocument();
45
+ });
46
+
47
+ it("renders actions when provided", () => {
48
+ render(
49
+ <StatePanel kind="empty" title="Empty" actions={<button type="button">New item</button>} />,
50
+ );
51
+ expect(screen.getByRole("button", { name: "New item" })).toBeInTheDocument();
52
+ });
53
+
54
+ it("renders custom icon when provided", () => {
55
+ render(
56
+ <StatePanel
57
+ kind="empty"
58
+ title="Custom icon"
59
+ icon={<span data-testid="custom-icon">icon</span>}
60
+ />,
61
+ );
62
+ expect(screen.getByTestId("custom-icon")).toBeInTheDocument();
63
+ });
64
+ });
@@ -0,0 +1,167 @@
1
+ import { type ReactNode } from "react";
2
+ import { CircleAlert } from "lucide-react";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { cn } from "../../lib/cn";
5
+
6
+ // ─── Variants ────────────────────────────────────────────────────────────────
7
+
8
+ export const statePanelVariants = cva(
9
+ [
10
+ "flex flex-col items-center justify-center gap-3 rounded-xl px-6 py-12 text-center",
11
+ "animate-in fade-in slide-in-from-bottom-2 duration-base ease-entrance",
12
+ ],
13
+ {
14
+ variants: {
15
+ kind: {
16
+ empty: "border border-dashed bg-surface",
17
+ // error: primary cue is color (destructive bg/border); structural cue
18
+ // (border-s-4 + border-border-strong top strip) survives monochrome /
19
+ // a monochrome palette so the panel is distinguishable from empty/loading even
20
+ // without hue. border-border-strong satisfies WCAG 1.4.11 non-text contrast.
21
+ error: "border border-destructive/30 bg-destructive/5 border-s-4 border-s-border-strong",
22
+ loading: "",
23
+ },
24
+ },
25
+ defaultVariants: { kind: "empty" },
26
+ },
27
+ );
28
+
29
+ // ─── Types ────────────────────────────────────────────────────────────────────
30
+
31
+ export interface StatePanelProps extends VariantProps<typeof statePanelVariants> {
32
+ /**
33
+ * The visual/semantic kind of state.
34
+ * - `"empty"` — neutral "nothing here yet" (dashed border, surface bg).
35
+ * - `"error"` — recoverable failure (destructive border/bg, alert role).
36
+ * - `"loading"` — in-progress (spinner, status/live-region role).
37
+ */
38
+ kind?: "empty" | "error" | "loading";
39
+ /** Heading text. */
40
+ title?: ReactNode;
41
+ /** Supporting description below the title. */
42
+ description?: ReactNode;
43
+ /**
44
+ * Icon or illustration shown above the title.
45
+ * For `"error"` defaults to a built-in warning triangle.
46
+ * For `"loading"` defaults to an animated spinner.
47
+ */
48
+ icon?: ReactNode;
49
+ /** Accessible label for the spinner in loading state. @default "Loading…" */
50
+ loadingLabel?: string;
51
+ /** Spinner size (loading kind only). @default "md" */
52
+ size?: "sm" | "md" | "lg";
53
+ /** Primary / secondary action buttons. */
54
+ actions?: ReactNode;
55
+ className?: string;
56
+ }
57
+
58
+ // ─── Default icons ────────────────────────────────────────────────────────────
59
+
60
+ const sizeMap: Record<string, string> = { sm: "size-4", md: "size-6", lg: "size-8" };
61
+
62
+ function DefaultSpinner({ size = "md" }: { size?: "sm" | "md" | "lg" }) {
63
+ return (
64
+ <svg
65
+ className={cn("animate-spin text-muted-foreground", sizeMap[size])}
66
+ viewBox="0 0 24 24"
67
+ fill="none"
68
+ aria-hidden="true"
69
+ >
70
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
71
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 0 1 8-8v4a4 4 0 0 0-4 4H4Z" />
72
+ </svg>
73
+ );
74
+ }
75
+
76
+ function DefaultErrorIcon() {
77
+ // Use the Lucide CircleAlert icon (solid ring + exclamation) for the error state.
78
+ // Rendered at size-10 so it reads clearly even at small panel sizes.
79
+ return <CircleAlert className="size-10" aria-hidden="true" />;
80
+ }
81
+
82
+ // ─── Component ───────────────────────────────────────────────────────────────
83
+
84
+ /**
85
+ * Unified state panel covering empty, error, and loading states.
86
+ * Replaces the three separate `EmptyState` / `ErrorState` / `LoadingState` components
87
+ * while keeping those as back-compat thin wrappers.
88
+ */
89
+ export function StatePanel({
90
+ kind = "empty",
91
+ title,
92
+ description,
93
+ icon,
94
+ loadingLabel = "Loading…",
95
+ size = "md",
96
+ actions,
97
+ className,
98
+ }: StatePanelProps) {
99
+ const isLoading = kind === "loading";
100
+ const isError = kind === "error";
101
+
102
+ // Role: loading → status + live region; error → alert; empty → (none needed).
103
+ const roleProps = isLoading
104
+ ? ({ role: "status", "aria-live": "polite" } as const)
105
+ : isError
106
+ ? ({ role: "alert" } as const)
107
+ : {};
108
+
109
+ // Default title / description fallbacks for error state (mirroring old ErrorState).
110
+ const resolvedTitle = title ?? (isError ? "Something went wrong" : undefined);
111
+ const resolvedDescription =
112
+ description ?? (isError ? "An unexpected error occurred. Please try again." : undefined);
113
+
114
+ // Resolve displayed icon.
115
+ let resolvedIcon: ReactNode;
116
+ if (icon !== undefined) {
117
+ resolvedIcon = icon;
118
+ } else if (isLoading) {
119
+ resolvedIcon = <DefaultSpinner size={size} />;
120
+ } else if (isError) {
121
+ resolvedIcon = <DefaultErrorIcon />;
122
+ }
123
+
124
+ return (
125
+ <div data-kind={kind} className={cn(statePanelVariants({ kind }), className)} {...roleProps}>
126
+ {/* Error eyebrow: "Error" label shown above the icon so the panel reads
127
+ as an error structurally even in monochrome themes. The
128
+ eyebrow uses text-destructive which resolves to a drawable
129
+ foreground at high decoration — combined with the thick left border from
130
+ statePanelVariants it gives two non-color structural cues. */}
131
+ {isError && (
132
+ <span className="text-xs font-semibold uppercase tracking-widest text-destructive">
133
+ Error
134
+ </span>
135
+ )}
136
+
137
+ {resolvedIcon ? (
138
+ // Error icon keeps text-destructive (not muted) so it stays visible even
139
+ // when rendered at low contrast. In monochrome themes the icon shape
140
+ // (CircleAlert) carries the cue; the color is supplemental.
141
+ <div
142
+ className={cn("[&_svg]:size-10", isError ? "text-destructive" : "text-muted-foreground")}
143
+ >
144
+ {resolvedIcon}
145
+ </div>
146
+ ) : null}
147
+
148
+ {(resolvedTitle || resolvedDescription) && (
149
+ <div className="space-y-1">
150
+ {resolvedTitle && (
151
+ <h3 className="text-sm font-semibold text-foreground">{resolvedTitle}</h3>
152
+ )}
153
+ {resolvedDescription && (
154
+ <p className="mx-auto max-w-sm text-sm text-muted-foreground">{resolvedDescription}</p>
155
+ )}
156
+ </div>
157
+ )}
158
+
159
+ {/* Loading label (visually shown as supporting text; also the live-region content). */}
160
+ {isLoading && loadingLabel && (
161
+ <span className="text-sm text-muted-foreground">{loadingLabel}</span>
162
+ )}
163
+
164
+ {actions ? <div className="mt-1 flex items-center gap-2">{actions}</div> : null}
165
+ </div>
166
+ );
167
+ }
@@ -0,0 +1,18 @@
1
+ export {
2
+ StatusBadge,
3
+ StatusIcon,
4
+ statusBadgeVariants,
5
+ fromTimelineStatus,
6
+ isCustomStatus,
7
+ STATUSES,
8
+ STATUS_LABELS,
9
+ STATUS_TONES,
10
+ STATUS_ROLE,
11
+ STATUS_TONE_ICONS,
12
+ type Status,
13
+ type StatusBadgeProps,
14
+ type StatusIconProps,
15
+ type StatusTone,
16
+ type CustomStatus,
17
+ type TimelineStatus,
18
+ } from "./status-badge";