@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,198 @@
1
+ import {
2
+ forwardRef,
3
+ type ComponentPropsWithoutRef,
4
+ type ElementRef,
5
+ type HTMLAttributes,
6
+ } from "react";
7
+ import * as MenubarPrimitive from "@radix-ui/react-menubar";
8
+ import { Check, ChevronRight, Circle } from "lucide-react";
9
+ import { cn } from "../../lib/cn";
10
+
11
+ // Pure re-exports (not `const X = Primitive.Y`) so tsc need not emit a portable
12
+ // type name for these — avoids TS2742 against transitive @radix-ui/react-context.
13
+ export {
14
+ Menu as MenubarMenu,
15
+ Group as MenubarGroup,
16
+ Portal as MenubarPortal,
17
+ Sub as MenubarSub,
18
+ RadioGroup as MenubarRadioGroup,
19
+ } from "@radix-ui/react-menubar";
20
+
21
+ export const Menubar = forwardRef<
22
+ ElementRef<typeof MenubarPrimitive.Root>,
23
+ ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
24
+ >(function Menubar({ className, ...props }, ref) {
25
+ return (
26
+ <MenubarPrimitive.Root
27
+ ref={ref}
28
+ className={cn("flex h-9 items-center gap-1 rounded-md bg-card p-1 shadow-ring-sm", className)}
29
+ {...props}
30
+ />
31
+ );
32
+ });
33
+
34
+ export const MenubarTrigger = forwardRef<
35
+ ElementRef<typeof MenubarPrimitive.Trigger>,
36
+ ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
37
+ >(function MenubarTrigger({ className, ...props }, ref) {
38
+ return (
39
+ <MenubarPrimitive.Trigger
40
+ ref={ref}
41
+ className={cn(
42
+ "flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none transition-colors duration-fast focus:bg-accent data-[state=open]:bg-accent",
43
+ className,
44
+ )}
45
+ {...props}
46
+ />
47
+ );
48
+ });
49
+
50
+ export const MenubarContent = forwardRef<
51
+ ElementRef<typeof MenubarPrimitive.Content>,
52
+ ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
53
+ >(function MenubarContent(
54
+ { className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
55
+ ref,
56
+ ) {
57
+ return (
58
+ <MenubarPrimitive.Portal>
59
+ <MenubarPrimitive.Content
60
+ ref={ref}
61
+ align={align}
62
+ alignOffset={alignOffset}
63
+ sideOffset={sideOffset}
64
+ className={cn(
65
+ "z-50 min-w-[12rem] overflow-hidden rounded-md bg-popover p-1 text-popover-foreground shadow-ring-md",
66
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95",
67
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
68
+ className,
69
+ )}
70
+ {...props}
71
+ />
72
+ </MenubarPrimitive.Portal>
73
+ );
74
+ });
75
+
76
+ export const MenubarItem = forwardRef<
77
+ ElementRef<typeof MenubarPrimitive.Item>,
78
+ ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & { inset?: boolean }
79
+ >(function MenubarItem({ className, inset, ...props }, ref) {
80
+ return (
81
+ <MenubarPrimitive.Item
82
+ ref={ref}
83
+ className={cn(
84
+ "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors duration-fast focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
85
+ inset && "ps-8",
86
+ className,
87
+ )}
88
+ {...props}
89
+ />
90
+ );
91
+ });
92
+
93
+ export const MenubarSubTrigger = forwardRef<
94
+ ElementRef<typeof MenubarPrimitive.SubTrigger>,
95
+ ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & { inset?: boolean }
96
+ >(function MenubarSubTrigger({ className, inset, children, ...props }, ref) {
97
+ return (
98
+ <MenubarPrimitive.SubTrigger
99
+ ref={ref}
100
+ className={cn(
101
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors duration-fast focus:bg-accent data-[state=open]:bg-accent",
102
+ inset && "ps-8",
103
+ className,
104
+ )}
105
+ {...props}
106
+ >
107
+ {children}
108
+ <ChevronRight className="ms-auto size-4" />
109
+ </MenubarPrimitive.SubTrigger>
110
+ );
111
+ });
112
+
113
+ export const MenubarSubContent = forwardRef<
114
+ ElementRef<typeof MenubarPrimitive.SubContent>,
115
+ ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
116
+ >(function MenubarSubContent({ className, ...props }, ref) {
117
+ return (
118
+ <MenubarPrimitive.SubContent
119
+ ref={ref}
120
+ className={cn(
121
+ "z-50 min-w-[8rem] overflow-hidden rounded-md bg-popover p-1 text-popover-foreground shadow-ring-md",
122
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95",
123
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
124
+ className,
125
+ )}
126
+ {...props}
127
+ />
128
+ );
129
+ });
130
+
131
+ export const MenubarCheckboxItem = forwardRef<
132
+ ElementRef<typeof MenubarPrimitive.CheckboxItem>,
133
+ ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
134
+ >(function MenubarCheckboxItem({ className, children, checked, ...props }, ref) {
135
+ return (
136
+ <MenubarPrimitive.CheckboxItem
137
+ ref={ref}
138
+ checked={checked}
139
+ className={cn(
140
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-sm outline-none transition-colors duration-fast focus:bg-accent",
141
+ className,
142
+ )}
143
+ {...props}
144
+ >
145
+ <span className="absolute start-2 flex size-3.5 items-center justify-center">
146
+ <MenubarPrimitive.ItemIndicator>
147
+ <Check className="size-4" />
148
+ </MenubarPrimitive.ItemIndicator>
149
+ </span>
150
+ {children}
151
+ </MenubarPrimitive.CheckboxItem>
152
+ );
153
+ });
154
+
155
+ export const MenubarRadioItem = forwardRef<
156
+ ElementRef<typeof MenubarPrimitive.RadioItem>,
157
+ ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
158
+ >(function MenubarRadioItem({ className, children, ...props }, ref) {
159
+ return (
160
+ <MenubarPrimitive.RadioItem
161
+ ref={ref}
162
+ className={cn(
163
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-sm outline-none transition-colors duration-fast focus:bg-accent",
164
+ className,
165
+ )}
166
+ {...props}
167
+ >
168
+ <span className="absolute start-2 flex size-3.5 items-center justify-center">
169
+ <MenubarPrimitive.ItemIndicator>
170
+ <Circle className="size-2 fill-current" />
171
+ </MenubarPrimitive.ItemIndicator>
172
+ </span>
173
+ {children}
174
+ </MenubarPrimitive.RadioItem>
175
+ );
176
+ });
177
+
178
+ export const MenubarSeparator = forwardRef<
179
+ ElementRef<typeof MenubarPrimitive.Separator>,
180
+ ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
181
+ >(function MenubarSeparator({ className, ...props }, ref) {
182
+ return (
183
+ <MenubarPrimitive.Separator
184
+ ref={ref}
185
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
186
+ {...props}
187
+ />
188
+ );
189
+ });
190
+
191
+ export function MenubarShortcut({ className, ...props }: HTMLAttributes<HTMLSpanElement>) {
192
+ return (
193
+ <span
194
+ className={cn("ms-auto text-xs tracking-widest text-muted-foreground", className)}
195
+ {...props}
196
+ />
197
+ );
198
+ }
@@ -0,0 +1 @@
1
+ export { MetricCard, type MetricCardProps, type MetricCardEmphasis } from "./metric-card";
@@ -0,0 +1,224 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, within } from "storybook/test";
3
+ import { MetricCard } from "./metric-card";
4
+
5
+ const meta = {
6
+ title: "Core/MetricCard",
7
+ component: MetricCard,
8
+ tags: ["autodocs"],
9
+ parameters: {
10
+ layout: "padded",
11
+ docs: {
12
+ description: {
13
+ component:
14
+ "The canonical KPI tile, owned by @elabs-ai/components-ui (ADR 0012). @elabs-ai/components-charts re-exports this exact " +
15
+ "component as Charts/MetricCard — they are the same component, so import from @elabs-ai/components-ui for app " +
16
+ "UI, or from @elabs-ai/components-charts when already in a charts context. Don't fork a second KPI tile.",
17
+ },
18
+ },
19
+ },
20
+ argTypes: {
21
+ label: {
22
+ description: "Tile label — the metric name shown above the value.",
23
+ control: "text",
24
+ table: { category: "Content" },
25
+ },
26
+ value: {
27
+ description: "Primary metric value (string or ReactNode).",
28
+ control: "text",
29
+ table: { category: "Content" },
30
+ },
31
+ description: {
32
+ description: "Secondary context line under the value.",
33
+ control: "text",
34
+ table: { category: "Content" },
35
+ },
36
+ delta: {
37
+ description: 'Signed change string, e.g. "+12.4%". Direction colors it.',
38
+ control: "text",
39
+ table: { category: "Content" },
40
+ },
41
+ deltaDirection: {
42
+ description: "Direction of the delta — drives the arrow glyph and polarity color.",
43
+ control: { type: "radio" },
44
+ options: ["up", "down", "neutral"],
45
+ table: { category: "State" },
46
+ },
47
+ positiveIsGood: {
48
+ description: "Flip to false for metrics where increase is unfavorable (e.g. churn).",
49
+ control: "boolean",
50
+ table: { category: "Behavior" },
51
+ },
52
+ emphasis: {
53
+ description:
54
+ "`headline` uses the larger kpi text rung; `default` is the calm intermediate rung.",
55
+ control: { type: "radio" },
56
+ options: ["default", "headline"],
57
+ table: { category: "Appearance" },
58
+ },
59
+ icon: {
60
+ description: "Optional icon node shown to the right of the label.",
61
+ control: false,
62
+ table: { category: "Content" },
63
+ },
64
+ visual: {
65
+ description: "Optional inline visual (e.g. sparkline) shown under value/description.",
66
+ control: false,
67
+ table: { category: "Content" },
68
+ },
69
+ evidence: {
70
+ description:
71
+ "Optional grounding footer — connects a figure to its source (e.g. EvidenceChip).",
72
+ control: false,
73
+ table: { category: "Content" },
74
+ },
75
+ loading: {
76
+ description: "Loading vs ready — renders a layout-shaped skeleton at the same box height.",
77
+ control: "boolean",
78
+ table: { category: "State" },
79
+ },
80
+ announceLoading: {
81
+ description:
82
+ "Whether this tile announces its own loading state (set false when composed inside a container, e.g. MetricGrid, that already announces once).",
83
+ control: "boolean",
84
+ table: { category: "State" },
85
+ },
86
+ },
87
+ } satisfies Meta<typeof MetricCard>;
88
+ export default meta;
89
+ type Story = StoryObj<typeof meta>;
90
+
91
+ export const Default: Story = {
92
+ args: {
93
+ label: "Active users",
94
+ value: "24,512",
95
+ delta: "12.4%",
96
+ deltaDirection: "up",
97
+ },
98
+ };
99
+
100
+ export const WithDescription: Story = {
101
+ args: {
102
+ label: "Monthly Revenue",
103
+ value: "$84.2k",
104
+ description: "vs $80.9k last month",
105
+ delta: "4.1%",
106
+ deltaDirection: "up",
107
+ },
108
+ };
109
+
110
+ export const DeltaDown: Story = {
111
+ args: {
112
+ label: "Churn Rate",
113
+ value: "1.8%",
114
+ description: "0.3 pp worse than target",
115
+ delta: "0.3%",
116
+ deltaDirection: "down",
117
+ positiveIsGood: false,
118
+ },
119
+ };
120
+
121
+ export const DeltaUnfavorable: Story = {
122
+ args: {
123
+ label: "Open tickets",
124
+ value: "37",
125
+ description: "15 unassigned",
126
+ delta: "+9",
127
+ deltaDirection: "up",
128
+ positiveIsGood: false,
129
+ },
130
+ };
131
+
132
+ export const Neutral: Story = {
133
+ args: {
134
+ label: "Sessions",
135
+ value: "100",
136
+ delta: "0%",
137
+ deltaDirection: "neutral",
138
+ },
139
+ };
140
+
141
+ export const NoMetrics: Story = {
142
+ args: {
143
+ label: "Open gaps",
144
+ value: "2",
145
+ description: "in progress",
146
+ },
147
+ };
148
+
149
+ // emphasis="headline" — the answer-leading KPI rung (#191, research 11 §B.5):
150
+ // size/weight/space only, the polarity color system is unchanged.
151
+ export const Headline: Story = {
152
+ args: {
153
+ emphasis: "headline",
154
+ label: "Net revenue retention",
155
+ value: "118%",
156
+ delta: "+4.2pp",
157
+ deltaDirection: "up",
158
+ },
159
+ };
160
+
161
+ export const WithEvidence: Story = {
162
+ args: {
163
+ label: "Enterprise churn",
164
+ value: "1.8%",
165
+ delta: "0.4pp",
166
+ deltaDirection: "down",
167
+ positiveIsGood: false,
168
+ evidence: <span>Grounded in 3 sources</span>,
169
+ },
170
+ };
171
+
172
+ /**
173
+ * A NUMERIC `value` is compacted for reading and stays reachable in full: the
174
+ * tile renders its value as a real button that copies the exact figure. The
175
+ * accessible name carries the tile's own label, so two tiles that compact to the
176
+ * same string are still told apart by a screen reader.
177
+ */
178
+ export const NumericValue: Story = {
179
+ args: {
180
+ label: "Revenue",
181
+ value: 50012102.632741,
182
+ delta: "8.1%",
183
+ deltaDirection: "up",
184
+ },
185
+ play: async ({ canvasElement }) => {
186
+ const canvas = within(canvasElement);
187
+ // Compacted for reading…
188
+ await expect(canvas.getByText("50M")).toBeInTheDocument();
189
+ // …and the exact figure is one keyboard-reachable control away.
190
+ await expect(
191
+ canvas.getByRole("button", { name: /50M\s+Revenue, Copy exact value/i }),
192
+ ).toBeInTheDocument();
193
+ },
194
+ };
195
+
196
+ /** `valueFormat="number"` is the escape hatch — full grouped digits, no copy control. */
197
+ export const NumericUncompacted: Story = {
198
+ args: {
199
+ label: "Open tickets",
200
+ value: 1284,
201
+ valueFormat: "number",
202
+ },
203
+ };
204
+
205
+ /** Currency and percent read the same numeric seam. */
206
+ export const NumericCurrency: Story = {
207
+ args: {
208
+ label: "Pipeline",
209
+ value: 4820000,
210
+ valueFormat: "currency",
211
+ currency: "EUR",
212
+ },
213
+ };
214
+
215
+ /** Loading vs ready (#268) — a layout-shaped skeleton at the same box height as `Default`. */
216
+ export const Loading: Story = {
217
+ args: {
218
+ label: "Active users",
219
+ value: "24,512",
220
+ delta: "12.4%",
221
+ deltaDirection: "up",
222
+ loading: true,
223
+ },
224
+ };
@@ -0,0 +1,198 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { MetricCard } from "./metric-card";
4
+
5
+ describe("MetricCard", () => {
6
+ it("renders label and value", () => {
7
+ render(<MetricCard label="Revenue" value="$42,000" />);
8
+ expect(screen.getByText("Revenue")).toBeInTheDocument();
9
+ expect(screen.getByText("$42,000")).toBeInTheDocument();
10
+ });
11
+
12
+ it("renders description when passed", () => {
13
+ render(<MetricCard label="Revenue" value="$42,000" description="vs $40k last month" />);
14
+ expect(screen.getByText("vs $40k last month")).toBeInTheDocument();
15
+ });
16
+
17
+ it("does not render description when omitted", () => {
18
+ render(<MetricCard label="Revenue" value="$42,000" />);
19
+ expect(screen.queryByRole("paragraph")).not.toBeInTheDocument();
20
+ });
21
+
22
+ it("renders delta text when provided", () => {
23
+ render(<MetricCard label="Users" value="1,234" delta="+12.4%" deltaDirection="up" />);
24
+ expect(screen.getByText(/12\.4%/)).toBeInTheDocument();
25
+ });
26
+
27
+ it("renders an optional visual slot", () => {
28
+ render(<MetricCard label="Sales" value="99" visual={<div data-testid="sparkline" />} />);
29
+ expect(screen.getByTestId("sparkline")).toBeInTheDocument();
30
+ });
31
+
32
+ it("renders an optional icon slot", () => {
33
+ render(
34
+ <MetricCard label="Orders" value="7" icon={<svg data-testid="icon" aria-hidden="true" />} />,
35
+ );
36
+ expect(screen.getByTestId("icon")).toBeInTheDocument();
37
+ });
38
+
39
+ // Polarity (good/bad) must survive monochrome themes via a non-color channel:
40
+ // a stable data-polarity hook + AT aria-label.
41
+ it("marks a favorable delta with data-polarity=good and a favorable AT label", () => {
42
+ render(
43
+ <MetricCard label="Revenue" value="$1M" delta="+12.4%" deltaDirection="up" positiveIsGood />,
44
+ );
45
+ const el = screen.getByLabelText("up +12.4%, favorable");
46
+ expect(el).toHaveAttribute("data-polarity", "good");
47
+ });
48
+
49
+ it("marks an unfavorable delta with data-polarity=bad and an unfavorable AT label", () => {
50
+ render(
51
+ <MetricCard
52
+ label="Open tickets"
53
+ value="42"
54
+ delta="+5"
55
+ deltaDirection="up"
56
+ positiveIsGood={false}
57
+ />,
58
+ );
59
+ const el = screen.getByLabelText("up +5, unfavorable");
60
+ expect(el).toHaveAttribute("data-polarity", "bad");
61
+ });
62
+
63
+ it("marks a neutral delta with data-polarity=neutral", () => {
64
+ render(<MetricCard label="Sessions" value="100" delta="0%" deltaDirection="neutral" />);
65
+ const el = screen.getByText(/0%/).closest("[data-polarity]");
66
+ expect(el).toHaveAttribute("data-polarity", "neutral");
67
+ });
68
+
69
+ // emphasis axis (#191, research 11 §B.5 KPI-1): size/weight/space only — the
70
+ // headline rung is text-kpi; default stays on the calm intermediate rung.
71
+ it("renders the calm default value rung by default", () => {
72
+ render(<MetricCard label="Revenue" value="$1M" />);
73
+ const value = screen.getByText("$1M");
74
+ expect(value.className).toContain("text-2xl");
75
+ expect(value.className).not.toContain("text-kpi");
76
+ });
77
+
78
+ it("renders the headline kpi rung for emphasis=headline", () => {
79
+ render(<MetricCard emphasis="headline" label="Revenue" value="$1M" />);
80
+ const value = screen.getByText("$1M");
81
+ expect(value.className).toContain("text-kpi");
82
+ expect(value.className).not.toContain("text-2xl");
83
+ });
84
+
85
+ it("renders an optional evidence footer slot (#191, KPI-3)", () => {
86
+ render(
87
+ <MetricCard
88
+ label="Revenue"
89
+ value="$1M"
90
+ evidence={<span data-testid="evidence">3 sources</span>}
91
+ />,
92
+ );
93
+ expect(screen.getByTestId("evidence")).toBeInTheDocument();
94
+ });
95
+
96
+ // Loading vs ready (#268): a layout-shaped skeleton replaces the value/label/delta
97
+ // text, and the region announces once via a single role="status" live region.
98
+ describe("loading", () => {
99
+ it("renders a skeleton instead of the label/value/delta text", () => {
100
+ render(
101
+ <MetricCard label="Revenue" value="$42,000" delta="+12.4%" deltaDirection="up" loading />,
102
+ );
103
+ expect(screen.queryByText("Revenue")).not.toBeInTheDocument();
104
+ expect(screen.queryByText("$42,000")).not.toBeInTheDocument();
105
+ expect(screen.queryByText(/12\.4%/)).not.toBeInTheDocument();
106
+ });
107
+
108
+ it("renders exactly one status live region for the not-ready state", () => {
109
+ render(
110
+ <MetricCard label="Revenue" value="$42,000" delta="+12.4%" deltaDirection="up" loading />,
111
+ );
112
+ expect(screen.getAllByRole("status")).toHaveLength(1);
113
+ });
114
+
115
+ it("renders the real content (no status region) when not loading", () => {
116
+ render(<MetricCard label="Revenue" value="$42,000" />);
117
+ expect(screen.getByText("Revenue")).toBeInTheDocument();
118
+ expect(screen.queryByRole("status")).not.toBeInTheDocument();
119
+ });
120
+
121
+ it("suppresses its own live region when announceLoading is false (composed in a grid)", () => {
122
+ render(<MetricCard announceLoading={false} label="Revenue" loading value="$42,000" />);
123
+ expect(screen.queryByRole("status")).not.toBeInTheDocument();
124
+ });
125
+ });
126
+
127
+ // A KPI tile is a small box; `50012102.632741` does not fit in one. Numbers
128
+ // are compacted by default and the exact figure stays one click away. Every
129
+ // OTHER ReactNode passes through untouched — that is what makes the new
130
+ // default safe for the call sites that already format their own strings.
131
+ describe("value formatting", () => {
132
+ it("compacts a numeric value by default", () => {
133
+ render(<MetricCard label="Revenue" value={50012102.632741} />);
134
+ expect(screen.getByRole("button")).toHaveTextContent("50M");
135
+ });
136
+
137
+ it("leaves a pre-formatted string byte-identical", () => {
138
+ render(<MetricCard label="Revenue" value="$42,000" />);
139
+ expect(screen.getByText("$42,000")).toBeInTheDocument();
140
+ // No control, because there is nothing shortened to restore.
141
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
142
+ });
143
+
144
+ it("passes an element through untouched", () => {
145
+ render(<MetricCard label="Revenue" value={<span data-testid="custom">≈ 50M</span>} />);
146
+ expect(screen.getByTestId("custom")).toBeInTheDocument();
147
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
148
+ });
149
+
150
+ it("prints every digit for valueFormat=number", () => {
151
+ render(<MetricCard label="Revenue" value={1500000} valueFormat="number" />);
152
+ expect(screen.getByText("1,500,000")).toBeInTheDocument();
153
+ });
154
+
155
+ it("renders currency with the requested code", () => {
156
+ render(<MetricCard currency="EUR" label="Revenue" value={1234} valueFormat="currency" />);
157
+ expect(screen.getByRole("button")).toHaveTextContent("€");
158
+ });
159
+
160
+ it("hands a function full control and offers no copy affordance", () => {
161
+ render(
162
+ <MetricCard label="Revenue" value={1234} valueFormat={(n) => <span>~{n} units</span>} />,
163
+ );
164
+ expect(screen.getByText(/~1234 units/)).toBeInTheDocument();
165
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
166
+ });
167
+
168
+ it("offers the exact value on click when the display was shortened", () => {
169
+ render(<MetricCard label="Revenue" value={50012102.632741} />);
170
+ expect(screen.getByRole("button", { name: /50M.*Copy exact value/i })).toBeInTheDocument();
171
+ });
172
+
173
+ it("offers no copy affordance when the display already IS the exact value", () => {
174
+ // 42 renders as "42" — a button around an unshortened number is noise.
175
+ render(<MetricCard label="Orders" value={42} />);
176
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
177
+ });
178
+
179
+ // Two tiles in a grid can compact to the same display; the label is a
180
+ // sibling node, so without it in the hint both buttons announce alike.
181
+ it("names the copy button after the metric it belongs to", () => {
182
+ render(
183
+ <>
184
+ <MetricCard label="Revenue" value={1234567} />
185
+ <MetricCard label="Pipeline" value={1234567} />
186
+ </>,
187
+ );
188
+ expect(screen.getByRole("button", { name: /1\.2M.*Revenue/i })).toBeInTheDocument();
189
+ expect(screen.getByRole("button", { name: /1\.2M.*Pipeline/i })).toBeInTheDocument();
190
+ });
191
+
192
+ it("can be opted out of with copyExactValue={false}", () => {
193
+ render(<MetricCard copyExactValue={false} label="Revenue" value={50012102.632741} />);
194
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
195
+ expect(screen.getByText("50M")).toBeInTheDocument();
196
+ });
197
+ });
198
+ });