@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,262 @@
1
+ import { createRef } from "react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { render, screen } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+ import { FilterChip, ResultCount, ViewToolbar, ViewToolbarFilters } from "./view-toolbar";
6
+
7
+ describe("ViewToolbar", () => {
8
+ it("renders the row shell with stable data-slot seams", () => {
9
+ const { container } = render(
10
+ <ViewToolbar actions={<button type="button">New run</button>}>
11
+ <span>Left</span>
12
+ </ViewToolbar>,
13
+ );
14
+
15
+ expect(container.querySelector("[data-slot='view-toolbar']")).not.toBeNull();
16
+ expect(container.querySelector("[data-slot='view-toolbar-content']")).not.toBeNull();
17
+ expect(container.querySelector("[data-slot='view-toolbar-actions']")).not.toBeNull();
18
+ });
19
+
20
+ it("forwards its ref, merges className and spreads props onto the root", () => {
21
+ const ref = createRef<HTMLDivElement>();
22
+ render(<ViewToolbar ref={ref} className="custom-row" id="runs-toolbar" data-testid="row" />);
23
+
24
+ const root = screen.getByTestId("row");
25
+ expect(ref.current).toBe(root);
26
+ expect(root).toHaveAttribute("id", "runs-toolbar");
27
+ expect(root.className).toContain("custom-row");
28
+ // The row wraps rather than clipping — the narrow-width contract.
29
+ expect(root.className).toContain("flex-wrap");
30
+ });
31
+
32
+ it("omits the actions cluster when no actions are supplied", () => {
33
+ const { container } = render(<ViewToolbar>Left</ViewToolbar>);
34
+ expect(container.querySelector("[data-slot='view-toolbar-actions']")).toBeNull();
35
+ });
36
+
37
+ it("renders the info affordance only when `info` is supplied", async () => {
38
+ const { rerender } = render(<ViewToolbar>Left</ViewToolbar>);
39
+ expect(screen.queryByRole("button", { name: "About this view" })).toBeNull();
40
+
41
+ rerender(<ViewToolbar info="Everything that ran in the last 30 days.">Left</ViewToolbar>);
42
+ expect(screen.getByRole("button", { name: "About this view" })).toBeInTheDocument();
43
+ });
44
+
45
+ /**
46
+ * The narrow-width AC in DOM terms: nothing between the action and the row may
47
+ * carry `overflow: hidden`, which is what would put the action cluster out of
48
+ * reach once the row wraps. (jsdom has no layout, so the geometric half of the
49
+ * assertion lives in the `Overflowing` story's play function, which runs in a
50
+ * real browser.)
51
+ */
52
+ it("never clips the action cluster out of the row", () => {
53
+ const { container } = render(
54
+ <ViewToolbar
55
+ info="Runs"
56
+ actions={
57
+ <button type="button" data-testid="primary-action">
58
+ New run
59
+ </button>
60
+ }
61
+ >
62
+ <ViewToolbarFilters>
63
+ <FilterChip label="Status: Failed" onRemove={() => undefined} />
64
+ </ViewToolbarFilters>
65
+ <ResultCount count={3} total={128} />
66
+ </ViewToolbar>,
67
+ );
68
+
69
+ const root = container.querySelector("[data-slot='view-toolbar']") as HTMLElement;
70
+ const action = screen.getByTestId("primary-action");
71
+
72
+ for (let node: HTMLElement | null = action; node && node !== root; node = node.parentElement) {
73
+ expect(node.className).not.toMatch(/\boverflow-hidden\b/);
74
+ }
75
+ expect(root.className).not.toMatch(/\boverflow-hidden\b/);
76
+ });
77
+
78
+ /**
79
+ * The other half of the narrow-width AC, and the reason the DOM assertion
80
+ * above is not enough: a `shrink-0` action cluster whose buttons cannot wrap
81
+ * does not get clipped — it OVERFLOWS the row, escaping the row's box by
82
+ * ~8px at a 320px viewport. The cluster must be able to shrink AND wrap.
83
+ * (The geometric proof runs in a real browser — see the `Overflowing`
84
+ * story's play function; jsdom has no layout.)
85
+ */
86
+ it("lets the action cluster wrap instead of overflowing the row", () => {
87
+ const { container } = render(
88
+ <ViewToolbar actions={<button type="button">New run</button>}>Left</ViewToolbar>,
89
+ );
90
+
91
+ const actions = container.querySelector("[data-slot='view-toolbar-actions']") as HTMLElement;
92
+ expect(actions.className).toMatch(/\bflex-wrap\b/);
93
+ expect(actions.className).not.toMatch(/\bshrink-0\b/);
94
+ });
95
+ });
96
+
97
+ describe("ViewToolbarFilters", () => {
98
+ it("names the chip run as a group so it is findable in assistive tech", () => {
99
+ render(
100
+ <ViewToolbarFilters>
101
+ <FilterChip label="Status: Failed" onRemove={() => undefined} />
102
+ </ViewToolbarFilters>,
103
+ );
104
+
105
+ expect(screen.getByRole("group", { name: "Active filters" })).toBeInTheDocument();
106
+ });
107
+
108
+ it("renders 'Clear all' only when onClearAll is supplied, and calls it", async () => {
109
+ const onClearAll = vi.fn();
110
+ const { rerender } = render(
111
+ <ViewToolbarFilters>
112
+ <FilterChip label="Status: Failed" onRemove={() => undefined} />
113
+ </ViewToolbarFilters>,
114
+ );
115
+ expect(screen.queryByRole("button", { name: "Clear all" })).toBeNull();
116
+
117
+ rerender(
118
+ <ViewToolbarFilters onClearAll={onClearAll}>
119
+ <FilterChip label="Status: Failed" onRemove={() => undefined} />
120
+ </ViewToolbarFilters>,
121
+ );
122
+ await userEvent.click(screen.getByRole("button", { name: "Clear all" }));
123
+ expect(onClearAll).toHaveBeenCalledTimes(1);
124
+ });
125
+
126
+ it("lets a caller override the group's accessible name", () => {
127
+ render(
128
+ <ViewToolbarFilters aria-label="Active run filters">
129
+ <FilterChip label="Status: Failed" onRemove={() => undefined} />
130
+ </ViewToolbarFilters>,
131
+ );
132
+ expect(screen.getByRole("group", { name: "Active run filters" })).toBeInTheDocument();
133
+ });
134
+ });
135
+
136
+ describe("FilterChip", () => {
137
+ it("renders label-in-value text verbatim", () => {
138
+ render(<FilterChip label="Status: Failed" onRemove={() => undefined} />);
139
+ expect(screen.getByText("Status: Failed")).toBeInTheDocument();
140
+ });
141
+
142
+ it("is a real button whose accessible name contains the visible label", () => {
143
+ render(<FilterChip label="Status: Failed" onRemove={() => undefined} />);
144
+ const chip = screen.getByRole("button", { name: "Remove filter: Status: Failed" });
145
+ expect(chip.tagName).toBe("BUTTON");
146
+ // WCAG 2.5.3 Label in Name.
147
+ expect(chip).toHaveAccessibleName(expect.stringContaining("Status: Failed"));
148
+ });
149
+
150
+ it("calls onRemove on click", async () => {
151
+ const onRemove = vi.fn();
152
+ render(<FilterChip label="Status: Failed" onRemove={onRemove} />);
153
+ await userEvent.click(screen.getByRole("button", { name: "Remove filter: Status: Failed" }));
154
+ expect(onRemove).toHaveBeenCalledTimes(1);
155
+ });
156
+
157
+ it("calls onRemove on Enter", async () => {
158
+ const onRemove = vi.fn();
159
+ render(<FilterChip label="Status: Failed" onRemove={onRemove} />);
160
+ screen.getByRole("button", { name: "Remove filter: Status: Failed" }).focus();
161
+ await userEvent.keyboard("{Enter}");
162
+ expect(onRemove).toHaveBeenCalledTimes(1);
163
+ });
164
+
165
+ it("calls onRemove on Space", async () => {
166
+ const onRemove = vi.fn();
167
+ render(<FilterChip label="Status: Failed" onRemove={onRemove} />);
168
+ screen.getByRole("button", { name: "Remove filter: Status: Failed" }).focus();
169
+ await userEvent.keyboard(" ");
170
+ expect(onRemove).toHaveBeenCalledTimes(1);
171
+ });
172
+
173
+ it("is reachable by keyboard", async () => {
174
+ render(
175
+ <ViewToolbarFilters>
176
+ <FilterChip label="Status: Failed" onRemove={() => undefined} />
177
+ <FilterChip label="Owner: Data platform" onRemove={() => undefined} />
178
+ </ViewToolbarFilters>,
179
+ );
180
+
181
+ await userEvent.tab();
182
+ expect(screen.getByRole("button", { name: "Remove filter: Status: Failed" })).toHaveFocus();
183
+ await userEvent.tab();
184
+ expect(
185
+ screen.getByRole("button", { name: "Remove filter: Owner: Data platform" }),
186
+ ).toHaveFocus();
187
+ });
188
+
189
+ it("lets a caller's onClick run first and cancel the removal", async () => {
190
+ const onRemove = vi.fn();
191
+ render(
192
+ <FilterChip
193
+ label="Status: Failed"
194
+ onRemove={onRemove}
195
+ onClick={(event) => event.preventDefault()}
196
+ />,
197
+ );
198
+ await userEvent.click(screen.getByRole("button", { name: "Remove filter: Status: Failed" }));
199
+ expect(onRemove).not.toHaveBeenCalled();
200
+ });
201
+
202
+ it("carries the view-toolbar-filter-chip slot and forwards its ref", () => {
203
+ const ref = createRef<HTMLButtonElement>();
204
+ const { container } = render(
205
+ <FilterChip ref={ref} label="Status: Failed" onRemove={() => undefined} />,
206
+ );
207
+ const chip = container.querySelector("[data-slot='view-toolbar-filter-chip']");
208
+ expect(chip).not.toBeNull();
209
+ expect(ref.current).toBe(chip);
210
+ });
211
+ });
212
+
213
+ describe("ResultCount", () => {
214
+ it("renders 'N of M' when both counts are supplied", () => {
215
+ const { container } = render(<ResultCount count={24} total={128} />);
216
+ expect(container.textContent).toBe("24 of 128");
217
+ });
218
+
219
+ it("falls back to a bare count when no total is supplied", () => {
220
+ const { container } = render(<ResultCount count={128} />);
221
+ expect(container.textContent).toBe("128");
222
+ });
223
+
224
+ it("appends an optional trailing noun", () => {
225
+ const { container } = render(
226
+ <ResultCount count={24} total={128}>
227
+ runs
228
+ </ResultCount>,
229
+ );
230
+ // Joined with a NON-BREAKING space (U+00A0, written as an escape so the
231
+ // assertion cannot be silently "fixed" into a plain space): the number must
232
+ // never wrap off its noun. See the micro-typography rule.
233
+ expect(container.textContent).toBe(`24 of 128\u00A0runs`);
234
+ });
235
+
236
+ it("renders 0 honestly rather than hiding an emptied view", () => {
237
+ const { container } = render(<ResultCount count={0} total={128} />);
238
+ expect(container.textContent).toBe("0 of 128");
239
+ });
240
+
241
+ it("uses tabular figures so counts do not jitter", () => {
242
+ const { container } = render(<ResultCount count={24} total={128} />);
243
+ const span = container.querySelector("[data-slot='view-toolbar-result-count']") as HTMLElement;
244
+ expect(span.className).toContain("tabular-nums");
245
+ });
246
+
247
+ it("renders a placeholder — never a number — while loading", () => {
248
+ const { container } = render(<ResultCount count={0} total={128} loading />);
249
+ expect(container.textContent).not.toContain("0 of 128");
250
+ expect(screen.getByRole("status")).toBeInTheDocument();
251
+ expect(screen.getByText("Loading…")).toBeInTheDocument();
252
+ });
253
+
254
+ it("forwards its ref and keeps the slot in both states", () => {
255
+ const ref = createRef<HTMLSpanElement>();
256
+ const { container, rerender } = render(<ResultCount ref={ref} count={24} total={128} />);
257
+ expect(ref.current).toBe(container.querySelector("[data-slot='view-toolbar-result-count']"));
258
+
259
+ rerender(<ResultCount ref={ref} count={24} total={128} loading />);
260
+ expect(ref.current).toBe(container.querySelector("[data-slot='view-toolbar-result-count']"));
261
+ });
262
+ });
@@ -0,0 +1,320 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ViewToolbar — the GRAMMAR for the one row that sits above every list, table
5
+ * or board view (#331).
6
+ *
7
+ * The failure this fixes was ambiguity, not capability: with no named row
8
+ * contract, ~40 routes of one consuming app grew 6 filter-chip idioms, 5
9
+ * result-count renderings and ~11px of control-height scatter in a single filter
10
+ * row. So the deliverable is a small, closed vocabulary — not a configurable
11
+ * widget:
12
+ *
13
+ * ⓘ info │ ── left cluster: status · filters · count ── │ actions
14
+ *
15
+ * - `ViewToolbar` is the row shell (slots only — it renders no view semantics).
16
+ * - `ViewToolbarFilters` is the ONE home for active-filter chips + "clear all".
17
+ * - `FilterChip` is the ONE removable chip (label-in-value, whole chip removes).
18
+ * - `ResultCount` is the ONE count (`tabular-nums`, "N of M" when filtered).
19
+ *
20
+ * Deliberate non-decisions, so a reader doesn't mistake them for oversights:
21
+ *
22
+ * - **No `cva`.** None of the four parts has a second visual axis today, and the
23
+ * rules forbid dead variants. A sticky/bounded bar is a caller `className`
24
+ * recipe (documented in `Docs/View Toolbar Contract`), not a variant.
25
+ * - **No `role="toolbar"` on the root.** That role promises roving-tabindex
26
+ * arrow-key navigation, which this row does not implement; claiming it would
27
+ * mislead assistive tech. Every control is an ordinary tab stop instead, and
28
+ * the one region that benefits from a name — the filter set — carries
29
+ * `role="group"` + an accessible name.
30
+ * - **The row wraps; it never scrolls or clips.** A hard fixed height would put
31
+ * the action cluster out of reach at narrow widths (AC), so the row keeps a
32
+ * `min-h-9` floor (== `Button` default height, so single-line rows are
33
+ * consistent) and wraps beyond it. The action cluster wraps INTERNALLY too —
34
+ * a `shrink-0` cluster whose buttons cannot wrap simply overflows the row's
35
+ * box once the row is narrower than the buttons are wide (measured: it
36
+ * escaped by ~8px at a 320px viewport), which is the very thing the AC
37
+ * forbids. It stays right-aligned (`justify-end`) on every line.
38
+ *
39
+ * Relationship to neighbours: `PageShell`'s generic `header` slot is where this
40
+ * row goes on a page (#367's toolbar header variant should PLACE a
41
+ * `<ViewToolbar>`, not invent a second row concept). `@elabs-ai/components-data`'s `FilterBar`
42
+ * stays the DataTable-scoped toolbar render-prop row; it could later be
43
+ * recomposed on this shell, but that is a separate change.
44
+ */
45
+ import { forwardRef, type ButtonHTMLAttributes, type HTMLAttributes, type ReactNode } from "react";
46
+ import { Info, X } from "lucide-react";
47
+ import { badgeVariants } from "../badge/badge";
48
+ import { Button } from "../button";
49
+ import { Skeleton } from "../skeleton";
50
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../tooltip";
51
+ import { useLocale } from "../locale-provider";
52
+ import { cn } from "../../lib/cn";
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // ViewToolbar (the row shell)
56
+ // ---------------------------------------------------------------------------
57
+
58
+ export interface ViewToolbarProps extends HTMLAttributes<HTMLDivElement> {
59
+ /**
60
+ * What this view is, as a tooltip on an ⓘ trigger at the start of the row —
61
+ * so a view does not need an in-page `<h1>` + paragraph block just to explain
62
+ * itself. Omit it when the surrounding page already says what the view is.
63
+ *
64
+ * **Pointer/keyboard only.** A Radix tooltip opens on hover and on keyboard
65
+ * focus but deliberately NOT on tap (and Radix suppresses the focus-open when
66
+ * focus arrived from a pointer), so on a touch device this text is
67
+ * unreachable. Treat `info` as progressive enhancement: never put something a
68
+ * phone user cannot do without in it. See `Docs/View Toolbar Contract`.
69
+ */
70
+ info?: ReactNode;
71
+ /** Left cluster — status/context, `ViewToolbarFilters`, `ResultCount`. */
72
+ children?: ReactNode;
73
+ /** Right cluster — the view's actions. Stays reachable at every width. */
74
+ actions?: ReactNode;
75
+ }
76
+
77
+ export const ViewToolbar = forwardRef<HTMLDivElement, ViewToolbarProps>(function ViewToolbar(
78
+ { info, children, actions, className, ...props },
79
+ ref,
80
+ ) {
81
+ const { t } = useLocale();
82
+
83
+ return (
84
+ <div
85
+ ref={ref}
86
+ data-slot="view-toolbar"
87
+ className={cn("flex min-h-9 w-full flex-wrap items-center gap-x-3 gap-y-2", className)}
88
+ {...props}
89
+ >
90
+ {info ? (
91
+ <TooltipProvider>
92
+ <Tooltip>
93
+ <TooltipTrigger asChild>
94
+ <Button
95
+ variant="ghost"
96
+ size="icon-sm"
97
+ data-slot="view-toolbar-info"
98
+ aria-label={t("ui.viewToolbar.about")}
99
+ className="shrink-0 text-muted-foreground hover:text-foreground"
100
+ >
101
+ <Info aria-hidden="true" />
102
+ </Button>
103
+ </TooltipTrigger>
104
+ <TooltipContent className="max-w-xs text-pretty">{info}</TooltipContent>
105
+ </Tooltip>
106
+ </TooltipProvider>
107
+ ) : null}
108
+
109
+ {/*
110
+ * Always rendered, even when empty: it owns the free space (`flex-1`), so
111
+ * the actions cluster stays anchored to the row's end, and it is a stable
112
+ * `data-slot` seam for consumers/tests.
113
+ *
114
+ * `basis-64` when it HAS content is what makes the narrow-width degrade
115
+ * work: with a 0 basis the left cluster would be squeezed to a few pixels
116
+ * so the actions could stay on line 1, shredding the chips. With a 16rem
117
+ * basis the two clusters stack instead. An EMPTY cluster keeps the 0 basis
118
+ * so it never forces a pointless wrap.
119
+ */}
120
+ <div
121
+ data-slot="view-toolbar-content"
122
+ className={cn(
123
+ "flex min-w-0 flex-1 flex-wrap items-center gap-x-3 gap-y-2",
124
+ children ? "basis-64" : "basis-0",
125
+ )}
126
+ >
127
+ {children}
128
+ </div>
129
+
130
+ {actions ? (
131
+ <div
132
+ data-slot="view-toolbar-actions"
133
+ className="ms-auto flex min-w-0 flex-wrap items-center justify-end gap-2"
134
+ >
135
+ {actions}
136
+ </div>
137
+ ) : null}
138
+ </div>
139
+ );
140
+ });
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // ViewToolbarFilters (the active-filter region)
144
+ // ---------------------------------------------------------------------------
145
+
146
+ export interface ViewToolbarFiltersProps extends HTMLAttributes<HTMLDivElement> {
147
+ /**
148
+ * Clear every active filter. Supplying it renders the "clear all" affordance
149
+ * at the end of the chip run; omit it when a view has no bulk reset.
150
+ */
151
+ onClearAll?: () => void;
152
+ }
153
+
154
+ /**
155
+ * The chip run. Render it ONLY when at least one filter is active — the
156
+ * "no filters applied" state is the region's ABSENCE, not an empty box.
157
+ *
158
+ * Runs at chip height (24px) so the whole group reads as one control rung; the
159
+ * `actions` cluster runs at button height. That split is the fix for the
160
+ * control-height scatter #331 measured.
161
+ */
162
+ export const ViewToolbarFilters = forwardRef<HTMLDivElement, ViewToolbarFiltersProps>(
163
+ function ViewToolbarFilters({ onClearAll, children, className, ...props }, ref) {
164
+ const { t } = useLocale();
165
+
166
+ return (
167
+ <div
168
+ ref={ref}
169
+ data-slot="view-toolbar-filters"
170
+ role="group"
171
+ aria-label={t("ui.viewToolbar.activeFilters")}
172
+ className={cn("flex min-w-0 flex-wrap items-center gap-1.5", className)}
173
+ {...props}
174
+ >
175
+ {children}
176
+ {onClearAll ? (
177
+ <Button
178
+ variant="ghost"
179
+ size="sm"
180
+ data-slot="view-toolbar-clear-all"
181
+ className="h-6 px-2"
182
+ onClick={onClearAll}
183
+ >
184
+ {t("ui.viewToolbar.clearAll")}
185
+ </Button>
186
+ ) : null}
187
+ </div>
188
+ );
189
+ },
190
+ );
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // FilterChip
194
+ // ---------------------------------------------------------------------------
195
+
196
+ export interface FilterChipProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
197
+ /**
198
+ * Label-in-value text — `"Status: Failed"`, never `"Status = failed"` and
199
+ * never a bare `"Failed"`. The caller composes the string so the chip reads
200
+ * as a sentence fragment out of context (and in a screen reader).
201
+ */
202
+ label: string;
203
+ /** Remove this filter. The WHOLE chip is the remove control — see below. */
204
+ onRemove: () => void;
205
+ }
206
+
207
+ /**
208
+ * A removable active-filter chip.
209
+ *
210
+ * The whole chip is a single `<button>` whose action is "remove", rather than a
211
+ * span with a nested ✕: one tab stop instead of two, a ≥24px target (WCAG 2.5.8)
212
+ * with no dead zone, and Enter/Space for free from the native element. Its
213
+ * accessible name is "Remove filter: <label>", which CONTAINS the visible label
214
+ * (WCAG 2.5.3 Label in Name).
215
+ *
216
+ * A chip that must EDIT a filter rather than remove it is a different control —
217
+ * reach for `FacetFilter` (`@elabs-ai/components-data`), not a variant of this.
218
+ */
219
+ export const FilterChip = forwardRef<HTMLButtonElement, FilterChipProps>(function FilterChip(
220
+ { label, onRemove, className, onClick, ...props },
221
+ ref,
222
+ ) {
223
+ const { t } = useLocale();
224
+
225
+ return (
226
+ <button
227
+ ref={ref}
228
+ type="button"
229
+ data-slot="view-toolbar-filter-chip"
230
+ aria-label={t("ui.viewToolbar.removeFilter", { label })}
231
+ className={cn(
232
+ badgeVariants({ variant: "secondary" }),
233
+ "group min-h-6 max-w-full gap-1 ps-2.5 pe-1.5",
234
+ "transition-colors duration-fast ease-standard hover:bg-secondary/70",
235
+ // Badge's own `focus:` ring is dead weight on a <span> but would fire on
236
+ // every MOUSE click here; neutralise it and keep the focus-visible ring.
237
+ "focus:ring-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
238
+ "disabled:pointer-events-none disabled:opacity-50",
239
+ className,
240
+ )}
241
+ onClick={(event) => {
242
+ onClick?.(event);
243
+ if (!event.defaultPrevented) onRemove();
244
+ }}
245
+ {...props}
246
+ >
247
+ <span className="truncate">{label}</span>
248
+ <X
249
+ aria-hidden="true"
250
+ className="size-3 shrink-0 opacity-60 transition-opacity duration-fast ease-standard group-hover:opacity-100"
251
+ />
252
+ </button>
253
+ );
254
+ });
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // ResultCount
258
+ // ---------------------------------------------------------------------------
259
+
260
+ export interface ResultCountProps extends HTMLAttributes<HTMLSpanElement> {
261
+ /** How many rows/cards the view is showing right now. */
262
+ count: number;
263
+ /**
264
+ * The unfiltered total. Supplying it renders "N of M" — the honest reading
265
+ * when a filter is active. Omit it for an unfiltered view's bare count.
266
+ */
267
+ total?: number;
268
+ /**
269
+ * No count yet. Renders a layout-shaped placeholder instead of a `0` that
270
+ * lies while the data is in flight (see `.claude/rules/loading-states.md`).
271
+ */
272
+ loading?: boolean;
273
+ /**
274
+ * Optional trailing noun ("results", "runs", "rows") joined with a
275
+ * non-breaking space, so the number is never announced bare.
276
+ */
277
+ children?: ReactNode;
278
+ }
279
+
280
+ export const ResultCount = forwardRef<HTMLSpanElement, ResultCountProps>(function ResultCount(
281
+ { count, total, loading = false, children, className, ...props },
282
+ ref,
283
+ ) {
284
+ const { t, formatNumber } = useLocale();
285
+
286
+ if (loading) {
287
+ return (
288
+ <span
289
+ ref={ref}
290
+ data-slot="view-toolbar-result-count"
291
+ role="status"
292
+ className={cn("inline-flex items-center", className)}
293
+ {...props}
294
+ >
295
+ <span className="sr-only">{t("loading")}</span>
296
+ <Skeleton className="h-3 w-16" />
297
+ </span>
298
+ );
299
+ }
300
+
301
+ const text =
302
+ total === undefined
303
+ ? formatNumber(count)
304
+ : t("ui.viewToolbar.countOfTotal", {
305
+ count: formatNumber(count),
306
+ total: formatNumber(total),
307
+ });
308
+
309
+ return (
310
+ <span
311
+ ref={ref}
312
+ data-slot="view-toolbar-result-count"
313
+ className={cn("text-caption tabular-nums text-muted-foreground", className)}
314
+ {...props}
315
+ >
316
+ {text}
317
+ {children ? <>&nbsp;{children}</> : null}
318
+ </span>
319
+ );
320
+ });
@@ -0,0 +1,2 @@
1
+ export { VirtualSelect, type VirtualSelectProps, type VirtualSelectOption } from "./virtual-select";
2
+ export { useVirtualListbox } from "./use-virtual-listbox";