@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,146 @@
1
+ "use client";
2
+
3
+ import { type MouseEvent, type ReactNode } from "react";
4
+ import {
5
+ AlertDialog,
6
+ AlertDialogAction,
7
+ AlertDialogCancel,
8
+ AlertDialogContent,
9
+ AlertDialogDescription,
10
+ AlertDialogFooter,
11
+ AlertDialogHeader,
12
+ AlertDialogTitle,
13
+ } from "../alert-dialog";
14
+ import { buttonVariants } from "../button";
15
+ import { cn } from "../../lib/cn";
16
+ import { useLocale } from "../locale-provider";
17
+ import { Spinner } from "../spinner";
18
+
19
+ export interface ConfirmDialogProps {
20
+ /** Controlled open state — the app owns it. */
21
+ open: boolean;
22
+ /** Fires on cancel, Escape, and any other Radix dismissal. */
23
+ onOpenChange: (open: boolean) => void;
24
+ /** What is about to happen, as a question or a statement. */
25
+ title: ReactNode;
26
+ /**
27
+ * The consequence, spelled out. **Required**: `AlertDialogContent` is
28
+ * inaccessible to screen-reader users without a description (Radix warns), and
29
+ * a confirmation whose only text is its title cannot state what is at stake.
30
+ */
31
+ description: ReactNode;
32
+ /**
33
+ * The confirm action's label. States the CONSEQUENCE ("Delete skill"), never
34
+ * "OK" — a label that names the outcome is the last chance to catch a mistake.
35
+ * Defaults to a generic "Confirm", which you should almost always replace.
36
+ */
37
+ confirmLabel?: ReactNode;
38
+ /** The safe action's label. Defaults to "Cancel". */
39
+ cancelLabel?: ReactNode;
40
+ /** Runs when the confirm action is activated. */
41
+ onConfirm: () => void;
42
+ /** Runs when the safe action is activated (in addition to `onOpenChange`). */
43
+ onCancel?: () => void;
44
+ /** Visual tone of the CONFIRM action only. The dialog chrome never changes. */
45
+ tone?: "default" | "destructive";
46
+ /**
47
+ * The confirm request is in flight: the action swaps its label for a spinner
48
+ * and reports `aria-busy`. It deliberately stays ENABLED
49
+ * (`.claude/rules/interaction-guidelines.md`, Forms — a submit is never
50
+ * pre-disabled); repeat activations are ignored while it is set.
51
+ */
52
+ loading?: boolean;
53
+ /** Extra body content between the description and the actions. */
54
+ children?: ReactNode;
55
+ className?: string;
56
+ }
57
+
58
+ /**
59
+ * A destructive/irreversible confirmation (#341).
60
+ *
61
+ * Built on `AlertDialog`, not `Dialog`, and that is the entire point of it
62
+ * existing as a named component rather than a recipe: `AlertDialogContent`
63
+ * prevents its own open-autofocus and focuses the mounted `AlertDialogCancel`
64
+ * instead, so **initial focus lands on the safe action** — but only if a Cancel
65
+ * is actually mounted, which a hand-rolled confirmation routinely forgets (and a
66
+ * plain `Dialog` cannot do at all). `ConfirmDialog` always renders one, so the
67
+ * wrong choice is unrepresentable. It also inherits `AlertDialog`'s refusal to
68
+ * dismiss on an outside click.
69
+ *
70
+ * Cancel is FIRST in the DOM so assistive tech reads the safe action first;
71
+ * `AlertDialogFooter`'s `flex-col-reverse sm:flex-row sm:justify-end` still
72
+ * paints the confirm action bottom-right on every breakpoint.
73
+ *
74
+ * **Confirming does not close the dialog** — `onConfirm` fires and the app sets
75
+ * `open={false}` when it is ready (immediately for a synchronous action, after
76
+ * the request for an async one, which is what makes `loading` coherent).
77
+ * Cancelling closes through `onOpenChange(false)` as usual.
78
+ *
79
+ * ```tsx
80
+ * <ConfirmDialog
81
+ * open={open}
82
+ * onOpenChange={setOpen}
83
+ * tone="destructive"
84
+ * title={`Delete ${skill.name}?`}
85
+ * description={`Removing it also deletes its ${skill.runs} saved runs. This cannot be undone.`}
86
+ * confirmLabel={`Delete ${skill.name}`}
87
+ * onConfirm={() => { deleteSkill(); setOpen(false); }}
88
+ * />
89
+ * ```
90
+ */
91
+ export function ConfirmDialog({
92
+ open,
93
+ onOpenChange,
94
+ title,
95
+ description,
96
+ confirmLabel,
97
+ cancelLabel,
98
+ onConfirm,
99
+ onCancel,
100
+ tone = "default",
101
+ loading = false,
102
+ children,
103
+ className,
104
+ }: ConfirmDialogProps) {
105
+ const { t } = useLocale();
106
+
107
+ function handleConfirm(event: MouseEvent<HTMLButtonElement>) {
108
+ // Radix's Action is a `Dialog.Close`; preventing the default stops it
109
+ // closing so the app stays in charge of `open` (and an in-flight request
110
+ // keeps its spinner on screen).
111
+ event.preventDefault();
112
+ if (loading) return;
113
+ onConfirm();
114
+ }
115
+
116
+ return (
117
+ <AlertDialog open={open} onOpenChange={onOpenChange}>
118
+ <AlertDialogContent data-slot="confirm-dialog" className={className}>
119
+ <AlertDialogHeader>
120
+ <AlertDialogTitle data-slot="confirm-dialog-title">{title}</AlertDialogTitle>
121
+ <AlertDialogDescription data-slot="confirm-dialog-description">
122
+ {description}
123
+ </AlertDialogDescription>
124
+ </AlertDialogHeader>
125
+ {children}
126
+ <AlertDialogFooter>
127
+ <AlertDialogCancel data-slot="confirm-dialog-cancel" onClick={onCancel}>
128
+ {cancelLabel ?? t("ui.confirmDialog.cancel")}
129
+ </AlertDialogCancel>
130
+ <AlertDialogAction
131
+ data-slot="confirm-dialog-confirm"
132
+ className={cn(tone === "destructive" && buttonVariants({ variant: "destructive" }))}
133
+ aria-busy={loading || undefined}
134
+ onClick={handleConfirm}
135
+ >
136
+ {/* `text-current` so the spinner inherits the action's foreground
137
+ token instead of `Spinner`'s muted default, which would drop
138
+ below 3:1 on a filled button. */}
139
+ {loading ? <Spinner className="text-current" label={t("loading")} /> : null}
140
+ {confirmLabel ?? t("ui.confirmDialog.confirm")}
141
+ </AlertDialogAction>
142
+ </AlertDialogFooter>
143
+ </AlertDialogContent>
144
+ </AlertDialog>
145
+ );
146
+ }
@@ -0,0 +1,2 @@
1
+ export { ConfirmDialog } from "./confirm-dialog";
2
+ export type { ConfirmDialogProps } from "./confirm-dialog";
@@ -0,0 +1,70 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, waitFor, within } from "storybook/test";
3
+ import {
4
+ ContextMenu,
5
+ ContextMenuContent,
6
+ ContextMenuItem,
7
+ ContextMenuSeparator,
8
+ ContextMenuTrigger,
9
+ } from "./context-menu";
10
+ const meta = {
11
+ title: "Overlays/ContextMenu",
12
+ component: ContextMenu,
13
+ tags: ["autodocs"],
14
+ argTypes: {
15
+ open: {
16
+ description: "Controlled open state of the context menu.",
17
+ control: "boolean",
18
+ table: { category: "State" },
19
+ },
20
+ defaultOpen: {
21
+ description: "Uncontrolled initial open state.",
22
+ control: "boolean",
23
+ table: { category: "State" },
24
+ },
25
+ onOpenChange: {
26
+ description: "Callback when the open state changes.",
27
+ control: false,
28
+ table: { category: "Events" },
29
+ },
30
+ modal: {
31
+ description: "Whether the context menu is modal (traps focus). Default true.",
32
+ control: "boolean",
33
+ table: { category: "Behaviour" },
34
+ },
35
+ dir: {
36
+ description: "Reading direction for the context menu.",
37
+ control: { type: "select" },
38
+ options: ["ltr", "rtl"],
39
+ table: { category: "Behaviour" },
40
+ },
41
+ },
42
+ } satisfies Meta<typeof ContextMenu>;
43
+ export default meta;
44
+ type Story = StoryObj<typeof meta>;
45
+ export const Default: Story = {
46
+ render: () => (
47
+ <ContextMenu>
48
+ <ContextMenuTrigger className="flex h-32 w-72 items-center justify-center rounded-md border border-dashed text-body text-muted-foreground">
49
+ Right-click here
50
+ </ContextMenuTrigger>
51
+ <ContextMenuContent>
52
+ <ContextMenuItem>Back</ContextMenuItem>
53
+ <ContextMenuItem>Reload</ContextMenuItem>
54
+ <ContextMenuSeparator />
55
+ <ContextMenuItem>Inspect</ContextMenuItem>
56
+ </ContextMenuContent>
57
+ </ContextMenu>
58
+ ),
59
+ // Right-clicks the trigger to open the portalled context menu and confirms
60
+ // the menu items are visible, then presses Escape to dismiss.
61
+ play: async ({ canvas, canvasElement, userEvent }) => {
62
+ const trigger = canvas.getByText("Right-click here");
63
+ await userEvent.pointer({ target: trigger, keys: "[MouseRight]" });
64
+ const body = within(canvasElement.ownerDocument.body);
65
+ const menu = await body.findByRole("menu");
66
+ await waitFor(() => expect(within(menu).getByText("Back")).toBeVisible());
67
+ await waitFor(() => expect(within(menu).getByText("Reload")).toBeVisible());
68
+ await userEvent.keyboard("{Escape}");
69
+ },
70
+ };
@@ -0,0 +1,172 @@
1
+ import {
2
+ forwardRef,
3
+ type ComponentPropsWithoutRef,
4
+ type ElementRef,
5
+ type HTMLAttributes,
6
+ } from "react";
7
+ import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
8
+ import { Check, ChevronRight, Circle } from "lucide-react";
9
+ import { cn } from "../../lib/cn";
10
+
11
+ export const ContextMenu = ContextMenuPrimitive.Root;
12
+ export const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
13
+ export const ContextMenuGroup = ContextMenuPrimitive.Group;
14
+ export const ContextMenuSub = ContextMenuPrimitive.Sub;
15
+ export const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
16
+
17
+ export const ContextMenuSubTrigger = forwardRef<
18
+ ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
19
+ ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & { inset?: boolean }
20
+ >(function ContextMenuSubTrigger({ className, inset, children, ...props }, ref) {
21
+ return (
22
+ <ContextMenuPrimitive.SubTrigger
23
+ ref={ref}
24
+ className={cn(
25
+ "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",
26
+ inset && "ps-8",
27
+ className,
28
+ )}
29
+ {...props}
30
+ >
31
+ {children}
32
+ <ChevronRight className="ms-auto size-4" />
33
+ </ContextMenuPrimitive.SubTrigger>
34
+ );
35
+ });
36
+
37
+ export const ContextMenuSubContent = forwardRef<
38
+ ElementRef<typeof ContextMenuPrimitive.SubContent>,
39
+ ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
40
+ >(function ContextMenuSubContent({ className, ...props }, ref) {
41
+ return (
42
+ <ContextMenuPrimitive.SubContent
43
+ ref={ref}
44
+ className={cn(
45
+ "z-50 min-w-[8rem] overflow-hidden rounded-md bg-popover p-1 text-popover-foreground shadow-ring-md 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 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",
46
+ className,
47
+ )}
48
+ {...props}
49
+ />
50
+ );
51
+ });
52
+
53
+ export const ContextMenuContent = forwardRef<
54
+ ElementRef<typeof ContextMenuPrimitive.Content>,
55
+ ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
56
+ >(function ContextMenuContent({ className, ...props }, ref) {
57
+ return (
58
+ <ContextMenuPrimitive.Portal>
59
+ <ContextMenuPrimitive.Content
60
+ ref={ref}
61
+ className={cn(
62
+ "z-50 min-w-[8rem] overflow-hidden rounded-md bg-popover p-1 text-popover-foreground shadow-ring-md 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 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",
63
+ className,
64
+ )}
65
+ {...props}
66
+ />
67
+ </ContextMenuPrimitive.Portal>
68
+ );
69
+ });
70
+
71
+ export const ContextMenuItem = forwardRef<
72
+ ElementRef<typeof ContextMenuPrimitive.Item>,
73
+ ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & { inset?: boolean }
74
+ >(function ContextMenuItem({ className, inset, ...props }, ref) {
75
+ return (
76
+ <ContextMenuPrimitive.Item
77
+ ref={ref}
78
+ className={cn(
79
+ "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 [&_svg]:size-4",
80
+ inset && "ps-8",
81
+ className,
82
+ )}
83
+ {...props}
84
+ />
85
+ );
86
+ });
87
+
88
+ export const ContextMenuCheckboxItem = forwardRef<
89
+ ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
90
+ ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
91
+ >(function ContextMenuCheckboxItem({ className, children, checked, ...props }, ref) {
92
+ return (
93
+ <ContextMenuPrimitive.CheckboxItem
94
+ ref={ref}
95
+ checked={checked}
96
+ className={cn(
97
+ "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",
98
+ className,
99
+ )}
100
+ {...props}
101
+ >
102
+ <span className="absolute start-2 flex size-3.5 items-center justify-center">
103
+ <ContextMenuPrimitive.ItemIndicator>
104
+ <Check className="size-4" />
105
+ </ContextMenuPrimitive.ItemIndicator>
106
+ </span>
107
+ {children}
108
+ </ContextMenuPrimitive.CheckboxItem>
109
+ );
110
+ });
111
+
112
+ export const ContextMenuRadioItem = forwardRef<
113
+ ElementRef<typeof ContextMenuPrimitive.RadioItem>,
114
+ ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
115
+ >(function ContextMenuRadioItem({ className, children, ...props }, ref) {
116
+ return (
117
+ <ContextMenuPrimitive.RadioItem
118
+ ref={ref}
119
+ className={cn(
120
+ "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",
121
+ className,
122
+ )}
123
+ {...props}
124
+ >
125
+ <span className="absolute start-2 flex size-3.5 items-center justify-center">
126
+ <ContextMenuPrimitive.ItemIndicator>
127
+ <Circle className="size-2 fill-current" />
128
+ </ContextMenuPrimitive.ItemIndicator>
129
+ </span>
130
+ {children}
131
+ </ContextMenuPrimitive.RadioItem>
132
+ );
133
+ });
134
+
135
+ export const ContextMenuLabel = forwardRef<
136
+ ElementRef<typeof ContextMenuPrimitive.Label>,
137
+ ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & { inset?: boolean }
138
+ >(function ContextMenuLabel({ className, inset, ...props }, ref) {
139
+ return (
140
+ <ContextMenuPrimitive.Label
141
+ ref={ref}
142
+ className={cn(
143
+ "px-2 py-1.5 text-xs font-semibold text-muted-foreground",
144
+ inset && "ps-8",
145
+ className,
146
+ )}
147
+ {...props}
148
+ />
149
+ );
150
+ });
151
+
152
+ export const ContextMenuSeparator = forwardRef<
153
+ ElementRef<typeof ContextMenuPrimitive.Separator>,
154
+ ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
155
+ >(function ContextMenuSeparator({ className, ...props }, ref) {
156
+ return (
157
+ <ContextMenuPrimitive.Separator
158
+ ref={ref}
159
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
160
+ {...props}
161
+ />
162
+ );
163
+ });
164
+
165
+ export function ContextMenuShortcut({ className, ...props }: HTMLAttributes<HTMLSpanElement>) {
166
+ return (
167
+ <span
168
+ className={cn("ms-auto text-xs tracking-widest text-muted-foreground", className)}
169
+ {...props}
170
+ />
171
+ );
172
+ }
@@ -0,0 +1,16 @@
1
+ export {
2
+ ContextMenu,
3
+ ContextMenuTrigger,
4
+ ContextMenuGroup,
5
+ ContextMenuSub,
6
+ ContextMenuRadioGroup,
7
+ ContextMenuSubTrigger,
8
+ ContextMenuSubContent,
9
+ ContextMenuContent,
10
+ ContextMenuItem,
11
+ ContextMenuCheckboxItem,
12
+ ContextMenuRadioItem,
13
+ ContextMenuLabel,
14
+ ContextMenuSeparator,
15
+ ContextMenuShortcut,
16
+ } from "./context-menu";
@@ -0,0 +1,80 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, userEvent, within } from "storybook/test";
3
+ import { CopyableValue } from "./copyable-value";
4
+
5
+ const meta = {
6
+ title: "Display/CopyableValue",
7
+ component: CopyableValue,
8
+ tags: ["autodocs"],
9
+ parameters: {
10
+ docs: {
11
+ description: {
12
+ component:
13
+ "Shown short, copied exact. Numbers are formatted compact across the library — this keeps the unrounded figure one click (or one Enter) away.",
14
+ },
15
+ },
16
+ },
17
+ argTypes: {
18
+ value: {
19
+ description: "The exact text written to the clipboard.",
20
+ control: "text",
21
+ table: { category: "Content" },
22
+ },
23
+ children: {
24
+ description: "What the reader sees. Defaults to `value`.",
25
+ table: { category: "Content" },
26
+ },
27
+ hint: {
28
+ description: 'Screen-reader hint appended to the visible text. Default: "Copy exact value".',
29
+ control: "text",
30
+ table: { category: "Content" },
31
+ },
32
+ resetAfterMs: {
33
+ description: 'ms the "Copied" confirmation stays. Default: 1500.',
34
+ control: "number",
35
+ table: { category: "Behavior" },
36
+ },
37
+ },
38
+ } satisfies Meta<typeof CopyableValue>;
39
+ export default meta;
40
+ type Story = StoryObj<typeof meta>;
41
+
42
+ /** With no children the exact value is also what is shown. */
43
+ export const Default: Story = {
44
+ args: { value: "50012102.632741" },
45
+ };
46
+
47
+ /** The usual case: a compact display over an unrounded clipboard payload. */
48
+ export const Compacted: Story = {
49
+ args: { value: "50012102.632741", children: "50M" },
50
+ };
51
+
52
+ /** Inline inside a sentence — it reads as text, not as a link. */
53
+ export const InProse: Story = {
54
+ args: { value: "1500000" },
55
+ render: (args) => (
56
+ <p className="text-body text-foreground">
57
+ Revenue reached <CopyableValue {...args}>1.5M</CopyableValue> across the quarter.
58
+ </p>
59
+ ),
60
+ };
61
+
62
+ /** The hint is part of the accessible name, so voice control can say what it reads. */
63
+ export const CustomHint: Story = {
64
+ args: { value: "0.9612", children: "96.1%", hint: "Copy unrounded rate" },
65
+ };
66
+
67
+ /**
68
+ * Keyboard path: the value is a real button, so it is a tab stop and Enter copies.
69
+ * jsdom/Chromium may deny clipboard permission — the assertion is on the control's
70
+ * reachability and name, not on the clipboard contents.
71
+ */
72
+ export const KeyboardReachable: Story = {
73
+ args: { value: "50012102.632741", children: "50M" },
74
+ play: async ({ canvasElement }) => {
75
+ const canvas = within(canvasElement);
76
+ const button = canvas.getByRole("button", { name: /50M Copy exact value/i });
77
+ await userEvent.tab();
78
+ await expect(button).toHaveFocus();
79
+ },
80
+ };
@@ -0,0 +1,128 @@
1
+ /**
2
+ * copyable-value.test.tsx — "shown short, copied exact" is only defensible if
3
+ * the exact value is genuinely reachable, so the locks here are: the clipboard
4
+ * gets the UNROUNDED string, the visible text stays inside the accessible name
5
+ * (WCAG 2.5.3), the live region exists before the copy (ARIA22), and a missing
6
+ * clipboard degrades instead of throwing.
7
+ *
8
+ * `userEvent.setup()` installs its OWN clipboard stub, so every clipboard stub
9
+ * below is installed AFTER setup — otherwise the spy is silently replaced.
10
+ */
11
+ import { describe, expect, it, vi } from "vitest";
12
+ import { render, screen, fireEvent, waitFor } from "@testing-library/react";
13
+ import userEvent from "@testing-library/user-event";
14
+ import { CopyableValue } from "./copyable-value";
15
+
16
+ /** Install a clipboard stub for one test; returns the writeText spy. */
17
+ function stubClipboard(impl: () => Promise<void> = () => Promise.resolve()) {
18
+ const writeText = vi.fn(impl);
19
+ Object.defineProperty(navigator, "clipboard", {
20
+ value: { writeText },
21
+ configurable: true,
22
+ writable: true,
23
+ });
24
+ return writeText;
25
+ }
26
+
27
+ describe("CopyableValue", () => {
28
+ it("shows the children and copies the exact value", async () => {
29
+ const writeText = stubClipboard();
30
+ render(<CopyableValue value="50012102.632741">50M</CopyableValue>);
31
+
32
+ const button = screen.getByRole("button");
33
+ expect(button).toHaveTextContent("50M");
34
+
35
+ fireEvent.click(button);
36
+ await waitFor(() => {
37
+ expect(writeText).toHaveBeenCalledWith("50012102.632741");
38
+ });
39
+ });
40
+
41
+ it("falls back to the exact value as its own display", () => {
42
+ render(<CopyableValue value="1234" />);
43
+ expect(screen.getByRole("button")).toHaveTextContent("1234");
44
+ });
45
+
46
+ it("keeps the visible text inside the accessible name (WCAG 2.5.3)", () => {
47
+ render(<CopyableValue value="1500000">1.5M</CopyableValue>);
48
+ // The hint is APPENDED, never substituted — a voice-control user must be
49
+ // able to say what they read.
50
+ expect(screen.getByRole("button", { name: "1.5M Copy exact value" })).toBeInTheDocument();
51
+ });
52
+
53
+ it("accepts a custom hint", () => {
54
+ render(
55
+ <CopyableValue hint="Copy unrounded rate" value="0.9612">
56
+ 96.1%
57
+ </CopyableValue>,
58
+ );
59
+ expect(screen.getByRole("button", { name: "96.1% Copy unrounded rate" })).toBeInTheDocument();
60
+ });
61
+
62
+ it("mounts the live region before the copy so it can be announced (ARIA22)", async () => {
63
+ stubClipboard();
64
+ render(<CopyableValue value="42">42</CopyableValue>);
65
+
66
+ const status = screen.getByRole("status");
67
+ expect(status).toBeInTheDocument();
68
+ expect(status).toHaveTextContent("");
69
+
70
+ fireEvent.click(screen.getByRole("button"));
71
+ await waitFor(() => {
72
+ expect(screen.getByRole("status")).toHaveTextContent("Copied");
73
+ });
74
+ });
75
+
76
+ it("marks the copied state on the control", async () => {
77
+ stubClipboard();
78
+ render(<CopyableValue value="42">42</CopyableValue>);
79
+
80
+ const button = screen.getByRole("button");
81
+ expect(button).not.toHaveAttribute("data-copied");
82
+
83
+ fireEvent.click(button);
84
+ await waitFor(() => {
85
+ expect(button).toHaveAttribute("data-copied");
86
+ });
87
+ });
88
+
89
+ it("is a real tab stop and copies on Enter", async () => {
90
+ const user = userEvent.setup();
91
+ const writeText = stubClipboard();
92
+ render(<CopyableValue value="42">42</CopyableValue>);
93
+
94
+ await user.tab();
95
+ expect(screen.getByRole("button")).toHaveFocus();
96
+ await user.keyboard("{Enter}");
97
+ await waitFor(() => {
98
+ expect(writeText).toHaveBeenCalledWith("42");
99
+ });
100
+ });
101
+
102
+ it("lets a consumer's onClick pre-empt the copy", () => {
103
+ const writeText = stubClipboard();
104
+ render(
105
+ <CopyableValue
106
+ onClick={(event) => {
107
+ event.preventDefault();
108
+ }}
109
+ value="42"
110
+ >
111
+ 42
112
+ </CopyableValue>,
113
+ );
114
+
115
+ fireEvent.click(screen.getByRole("button"));
116
+ expect(writeText).not.toHaveBeenCalled();
117
+ });
118
+
119
+ it("degrades where there is no clipboard (insecure context / old browser)", async () => {
120
+ Object.defineProperty(navigator, "clipboard", { value: undefined, configurable: true });
121
+ render(<CopyableValue value="42">42</CopyableValue>);
122
+
123
+ fireEvent.click(screen.getByRole("button"));
124
+ await waitFor(() => {
125
+ expect(screen.getByRole("status")).toHaveTextContent("");
126
+ });
127
+ });
128
+ });