@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,463 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, userEvent, waitFor, within } from "storybook/test";
3
+ import { useId, useMemo, useState } from "react";
4
+ import { Label } from "../label";
5
+ import {
6
+ MentionInput,
7
+ MentionInputContent,
8
+ MentionInputEmpty,
9
+ MentionInputItem,
10
+ MentionInputList,
11
+ MentionInputTextarea,
12
+ } from "./mention-input";
13
+ import { serializeMentions, type MentionOption, type MentionValue } from "./mention-value";
14
+
15
+ const ROSTER: MentionOption[] = [
16
+ { id: "u-lovelace", label: "Ada Lovelace", description: "Analytical Engine", keywords: ["math"] },
17
+ { id: "u-hopper", label: "Grace Hopper", description: "Compilers", keywords: ["cobol"] },
18
+ { id: "u-johnson", label: "Katherine Johnson", description: "Orbital mechanics" },
19
+ { id: "u-noether", label: "Emmy Noether", description: "Abstract algebra" },
20
+ { id: "u-turing", label: "Alan Turing", description: "On sabbatical", disabled: true },
21
+ ];
22
+
23
+ /**
24
+ * `MentionInput` is an `@`-mention text field built on a **real `<textarea>`**.
25
+ *
26
+ * Typing the trigger character at the start of a word opens the component's own
27
+ * listbox; arrow keys move the highlight, Enter or Tab inserts, Escape dismisses
28
+ * — and **focus never leaves the field**, so the caret is never lost. An
29
+ * inserted mention behaves as one atomic unit: a single Backspace beside it
30
+ * removes the whole `@Name`, and the arrow keys step over it rather than into
31
+ * it.
32
+ *
33
+ * The value is `{ text, mentions }`; `serializeMentions()` turns it into the
34
+ * `{ text, mentionedIds }` payload an API wants.
35
+ */
36
+ const meta = {
37
+ title: "Forms/MentionInput",
38
+ component: MentionInput,
39
+ tags: ["autodocs"],
40
+ parameters: {
41
+ docs: {
42
+ description: {
43
+ component:
44
+ "An @-mention text field on a real <textarea>. Focus never leaves the field; " +
45
+ "mentions behave as atomic tokens; the value serializes to { text, mentionedIds }.",
46
+ },
47
+ },
48
+ },
49
+ argTypes: {
50
+ options: { control: false, table: { category: "Content" } },
51
+ value: { control: false, table: { category: "State" } },
52
+ defaultValue: { control: false, table: { category: "State" } },
53
+ open: { control: false, table: { category: "State" } },
54
+ trigger: { control: "text", table: { category: "Behavior" } },
55
+ filter: { control: false, table: { category: "Behavior" } },
56
+ },
57
+ } satisfies Meta<typeof MentionInput>;
58
+
59
+ export default meta;
60
+ type Story = StoryObj<typeof meta>;
61
+
62
+ /**
63
+ * The full compound arrangement every story below composes.
64
+ *
65
+ * Note the consumer-supplied `id` on `MentionInputTextarea`: it wins over the
66
+ * component's internal one (props are spread last), which is what keeps
67
+ * `<Label htmlFor>` working. `useId` rather than a literal because autodocs
68
+ * renders every story on one page and duplicate ids are an axe violation.
69
+ */
70
+ function Field({
71
+ options = ROSTER,
72
+ label = "Comment",
73
+ ...props
74
+ }: Partial<React.ComponentProps<typeof MentionInput>> & {
75
+ options?: MentionOption[];
76
+ label?: string;
77
+ }) {
78
+ const id = useId();
79
+ return (
80
+ <div className="w-[28rem] space-y-2">
81
+ <Label htmlFor={id}>{label}</Label>
82
+ <MentionInput options={options} {...props}>
83
+ <MentionInputTextarea id={id} placeholder="Type @ to mention someone…" />
84
+ <MentionInputContent>
85
+ <MentionInputList>
86
+ {(option) => <MentionInputItem key={option.id} option={option} />}
87
+ </MentionInputList>
88
+ <MentionInputEmpty />
89
+ </MentionInputContent>
90
+ </MentionInput>
91
+ </div>
92
+ );
93
+ }
94
+
95
+ /** Resting state — an empty field with nothing but its placeholder. */
96
+ export const Empty: Story = {
97
+ args: { options: ROSTER, children: null },
98
+ render: () => <Field />,
99
+ };
100
+
101
+ /** Ordinary prose already in the field, with the popup closed. */
102
+ export const Typing: Story = {
103
+ args: { options: ROSTER, children: null },
104
+ render: () => (
105
+ <Field defaultValue={{ text: "Thanks for the review — one follow-up ", mentions: [] }} />
106
+ ),
107
+ };
108
+
109
+ /**
110
+ * The popup open on a partial query, driven entirely from the keyboard. The
111
+ * play function types the trigger and arrows down, then asserts the field is
112
+ * still the focused element and that `aria-activedescendant` names the option
113
+ * carrying `aria-selected="true"`.
114
+ */
115
+ export const PopupOpen: Story = {
116
+ args: { options: ROSTER, children: null },
117
+ render: () => <Field />,
118
+ play: async ({ canvasElement, step }) => {
119
+ const canvas = within(canvasElement);
120
+ const field = canvas.getByRole("textbox", { name: "Comment" });
121
+
122
+ await step("typing the trigger opens the roster", async () => {
123
+ await userEvent.click(field);
124
+ await userEvent.keyboard("Nice work @");
125
+ await expect(field).toHaveAttribute("data-state", "open");
126
+ });
127
+
128
+ await step("arrow keys move the highlight without moving focus", async () => {
129
+ await userEvent.keyboard("{ArrowDown}");
130
+ await expect(field).toHaveFocus();
131
+
132
+ const activeId = field.getAttribute("aria-activedescendant");
133
+ await expect(activeId).toBeTruthy();
134
+ const active = document.getElementById(activeId!);
135
+ await expect(active).not.toBeNull();
136
+ await expect(active).toHaveAttribute("aria-selected", "true");
137
+ });
138
+ },
139
+ };
140
+
141
+ /**
142
+ * Two inserted mentions. Each `@Name` run is painted by an `aria-hidden` mirror
143
+ * layer sitting over the field, so it reads as a chip while the text underneath
144
+ * stays ordinary, selectable, spellcheckable textarea content.
145
+ *
146
+ * The play function proves atomicity: one Backspace beside a mention removes
147
+ * the entire token.
148
+ */
149
+ export const WithChips: Story = {
150
+ args: { options: ROSTER, children: null },
151
+ render: () => (
152
+ <Field
153
+ defaultValue={{
154
+ text: "@Ada Lovelace and @Grace Hopper should both see this before it ships.",
155
+ mentions: [
156
+ { id: "u-lovelace", label: "Ada Lovelace", start: 0 },
157
+ { id: "u-hopper", label: "Grace Hopper", start: 18 },
158
+ ],
159
+ }}
160
+ />
161
+ ),
162
+ play: async ({ canvasElement, step }) => {
163
+ const canvas = within(canvasElement);
164
+ const field = canvas.getByRole("textbox", { name: "Comment" }) as HTMLTextAreaElement;
165
+
166
+ await step("one Backspace removes the whole @Grace Hopper token", async () => {
167
+ await userEvent.click(field);
168
+ // Caret to the end offset of the second mention: 18 + "@Grace Hopper".length
169
+ field.setSelectionRange(31, 31);
170
+ field.dispatchEvent(new Event("select", { bubbles: true }));
171
+ await userEvent.keyboard("{Backspace}");
172
+ await expect(field.value).toBe("@Ada Lovelace and should both see this before it ships.");
173
+ });
174
+ },
175
+ };
176
+
177
+ /**
178
+ * What a consumer actually sends. `serializeMentions()` returns
179
+ * `{ text, mentionedIds }` with the ids **deduped** and in **document order**.
180
+ */
181
+ export const SerializedOutput: Story = {
182
+ args: { options: ROSTER, children: null },
183
+ render: function SerializedOutputStory() {
184
+ const id = useId();
185
+ const [value, setValue] = useState<MentionValue>({
186
+ text: "@Ada Lovelace can you review this with @Grace Hopper and @Ada Lovelace again?",
187
+ mentions: [
188
+ { id: "u-lovelace", label: "Ada Lovelace", start: 0 },
189
+ { id: "u-hopper", label: "Grace Hopper", start: 39 },
190
+ { id: "u-lovelace", label: "Ada Lovelace", start: 57 },
191
+ ],
192
+ });
193
+ const payload = useMemo(() => serializeMentions(value), [value]);
194
+
195
+ return (
196
+ <div className="w-[28rem] space-y-4">
197
+ <div className="space-y-2">
198
+ <Label htmlFor={id}>Comment</Label>
199
+ <MentionInput options={ROSTER} value={value} onValueChange={setValue}>
200
+ <MentionInputTextarea id={id} placeholder="Type @ to mention someone…" />
201
+ <MentionInputContent>
202
+ <MentionInputList>
203
+ {(option) => <MentionInputItem key={option.id} option={option} />}
204
+ </MentionInputList>
205
+ <MentionInputEmpty />
206
+ </MentionInputContent>
207
+ </MentionInput>
208
+ </div>
209
+ {/* A scrollable region needs to be keyboard-reachable (axe
210
+ `scrollable-region-focusable`), and a focusable region needs a name. */}
211
+ <pre
212
+ tabIndex={0}
213
+ role="region"
214
+ aria-label="Serialized payload"
215
+ className="overflow-x-auto rounded-md bg-surface-muted p-3 font-mono text-meta"
216
+ >
217
+ {JSON.stringify(payload, null, 2)}
218
+ </pre>
219
+ </div>
220
+ );
221
+ },
222
+ };
223
+
224
+ /**
225
+ * An app-side roster. The consumer passes `filter={() => true}` (the popup shows
226
+ * whatever it is given) and fetches from `onQueryChange`. There is deliberately
227
+ * no `loading` prop — render whatever placeholder the surface wants inside
228
+ * `MentionInputEmpty`.
229
+ */
230
+ export const AsyncRoster: Story = {
231
+ args: { options: ROSTER, children: null },
232
+ render: function AsyncRosterStory() {
233
+ const id = useId();
234
+ const [query, setQuery] = useState<string | null>(null);
235
+ const [options, setOptions] = useState<MentionOption[]>([]);
236
+
237
+ return (
238
+ <div className="w-[28rem] space-y-2">
239
+ <Label htmlFor={id}>Assign reviewers</Label>
240
+ <MentionInput
241
+ options={options}
242
+ filter={() => true}
243
+ onQueryChange={(next) => {
244
+ setQuery(next);
245
+ // Stands in for a request; the roster is owned by the app.
246
+ setOptions(
247
+ next === null
248
+ ? []
249
+ : ROSTER.filter((option) =>
250
+ option.label.toLowerCase().includes(next.toLowerCase()),
251
+ ),
252
+ );
253
+ }}
254
+ >
255
+ <MentionInputTextarea id={id} placeholder="Type @ to mention someone…" />
256
+ <MentionInputContent>
257
+ <MentionInputList>
258
+ {(option) => <MentionInputItem key={option.id} option={option} />}
259
+ </MentionInputList>
260
+ <MentionInputEmpty>
261
+ {query === null || query.length === 0 ? "Type to search…" : "No matches."}
262
+ </MentionInputEmpty>
263
+ </MentionInputContent>
264
+ </MentionInput>
265
+ <p className="text-meta text-muted-foreground">
266
+ Last query sent to the app: {query === null ? "— (popup closed)" : `"${query}"`}
267
+ </p>
268
+ </div>
269
+ );
270
+ },
271
+ };
272
+
273
+ /** A non-default trigger — the same machinery drives `#`-style references. */
274
+ export const CustomTrigger: Story = {
275
+ args: { options: ROSTER, children: null },
276
+ render: () => (
277
+ <Field
278
+ label="Link an issue"
279
+ trigger="#"
280
+ options={[
281
+ { id: "i-101", label: "Roadmap", description: "Milestone" },
282
+ { id: "i-102", label: "Retention", description: "Epic" },
283
+ { id: "i-103", label: "Rendering", description: "Epic" },
284
+ ]}
285
+ />
286
+ ),
287
+ };
288
+
289
+ /* -------------------------------------------------------------------------- */
290
+ /* Mirror re-measure regression locks */
291
+ /* */
292
+ /* The chip overlay copies the field's glyph metrics into an inline style. A */
293
+ /* ONE-SHOT snapshot is not enough: the first layout effect runs before the */
294
+ /* theme attribute is applied, so it captured the `:root` fallback font and the */
295
+ /* wash rendered ~14% wide in a different typeface. The component re-measures */
296
+ /* on a MutationObserver (root `data-theme`/`data-density`), a ResizeObserver */
297
+ /* (the field's box) and `document.fonts.ready`. */
298
+ /* */
299
+ /* They MUST run in a real browser: the drift was invisible to a */
300
+ /* self-consistent `getComputedStyle` read and jsdom has no layout at all. */
301
+ /* */
302
+ /* WHAT THESE ACTUALLY LOCK — measured by deleting each arm and re-running, */
303
+ /* twice per arm. Corrected in fix round 3: an earlier version of this block */
304
+ /* claimed `ResizeObserver` was the ONLY individually-pinnable arm and that it */
305
+ /* "re-measures in every scenario that could be constructed". A reviewer built */
306
+ /* a counterexample, so that generalisation was false — it described the */
307
+ /* scenarios that had been tried, not the code. */
308
+ /* */
309
+ /* * All three arms removed -> 3 stories RED with the ORIGINAL symptom */
310
+ /* (overlay stuck on `Inter` while the field renders IBM Plex Mono). */
311
+ /* * `ResizeObserver` alone removed -> `MirrorTracksFieldResize` RED. */
312
+ /* * `MutationObserver` alone removed -> the nailed-box theme-flip story */
313
+ /* RED. It lives in `mention-input-mirror.stories.tsx`, NOT here: with it */
314
+ /* co-resident in this file the same deletion came back green, because a */
315
+ /* preceding story in the shared page re-measured the mirror for an */
316
+ /* unrelated reason. Its own file is what makes the lock hold in CI. */
317
+ /* * `document.fonts.ready` alone removed -> everything still green. It */
318
+ /* resisted every scenario ATTEMPTED (theme flip; pinned box; nailed box; */
319
+ /* a field-level letter-spacing change; a late-loading face above the */
320
+ /* field shifting its offsetTop). That is a statement about those */
321
+ /* scenarios, not a proof the arm is dead — it is kept as defence in */
322
+ /* depth, and is covered compositely by the all-arms case above. */
323
+ /* -------------------------------------------------------------------------- */
324
+
325
+ const MIRROR_TEXT = "@Ada Lovelace ships";
326
+ const MIRROR_VALUE: MentionValue = {
327
+ text: MIRROR_TEXT,
328
+ mentions: [{ id: "u-lovelace", label: "Ada Lovelace", start: 0 }],
329
+ };
330
+
331
+ /**
332
+ * **Regression lock — the mirror follows a resize of the field.**
333
+ *
334
+ * Narrows an ancestor that sits OUTSIDE the `MentionInput` root, so neither the
335
+ * field's own attributes nor the document root's change — the `MutationObserver`
336
+ * arms are structurally blind to it and only the `ResizeObserver` can keep the
337
+ * overlay's box on top of the field's.
338
+ */
339
+ export const MirrorTracksFieldResize: Story = {
340
+ args: { options: ROSTER, children: null },
341
+ render: function MirrorResizeStory() {
342
+ const id = useId();
343
+ return (
344
+ <div data-testid="resize-host" style={{ width: "28rem" }} className="space-y-2">
345
+ <Label htmlFor={id}>Comment</Label>
346
+ <MentionInput options={ROSTER} defaultValue={MIRROR_VALUE}>
347
+ <MentionInputTextarea id={id} placeholder="Type @ to mention someone…" />
348
+ <MentionInputContent>
349
+ <MentionInputList>
350
+ {(option) => <MentionInputItem key={option.id} option={option} />}
351
+ </MentionInputList>
352
+ <MentionInputEmpty />
353
+ </MentionInputContent>
354
+ </MentionInput>
355
+ </div>
356
+ );
357
+ },
358
+ play: async ({ canvasElement, step }) => {
359
+ const field = within(canvasElement).getByRole("textbox", {
360
+ name: "Comment",
361
+ }) as HTMLTextAreaElement;
362
+ const overlay = canvasElement.querySelector<HTMLElement>('[data-slot="mention-input-overlay"]');
363
+ const host = canvasElement.querySelector<HTMLElement>('[data-testid="resize-host"]');
364
+ await expect(overlay).not.toBeNull();
365
+ await expect(host).not.toBeNull();
366
+
367
+ await step("the mirror starts on top of the field", async () => {
368
+ await expect(overlay!.getBoundingClientRect().width).toBeCloseTo(
369
+ field.getBoundingClientRect().width,
370
+ 0,
371
+ );
372
+ });
373
+
374
+ await step("narrow an ancestor outside the MentionInput root", async () => {
375
+ const widthBefore = field.getBoundingClientRect().width;
376
+ host!.style.width = "18rem";
377
+ await waitFor(() => {
378
+ expect(field.getBoundingClientRect().width).toBeLessThan(widthBefore - 20);
379
+ });
380
+ });
381
+
382
+ await step("the mirror re-measures onto the new box", async () => {
383
+ await waitFor(() => {
384
+ const f = field.getBoundingClientRect();
385
+ const o = overlay!.getBoundingClientRect();
386
+ expect(o.width).toBeCloseTo(f.width, 0);
387
+ expect(o.height).toBeCloseTo(f.height, 0);
388
+ expect(o.left).toBeCloseTo(f.left, 0);
389
+ expect(o.top).toBeCloseTo(f.top, 0);
390
+ });
391
+ });
392
+ },
393
+ };
394
+
395
+ /**
396
+ * **Regression lock — the mirror follows a change to the FIELD's own style.**
397
+ *
398
+ * A third, font-free lever: `letter-spacing` changes glyph advance while the
399
+ * pinned box stays put (the play function asserts the box did not move) and no
400
+ * font loads. It was written to isolate the `MutationObserver` arm — and it does
401
+ * not: measured, it still passes with that observer deleted, so `ResizeObserver`
402
+ * reacts here too. Kept because it is a third independent scenario in the
403
+ * all-arms lock, and because a consumer restyling the field is a real case.
404
+ */
405
+ export const MirrorTracksFieldStyleChange: Story = {
406
+ args: { options: ROSTER, children: null },
407
+ render: function MirrorStyleStory() {
408
+ const id = useId();
409
+ return (
410
+ <div className="space-y-2">
411
+ <Label htmlFor={id}>Comment</Label>
412
+ <MentionInput options={ROSTER} defaultValue={MIRROR_VALUE}>
413
+ <MentionInputTextarea
414
+ id={id}
415
+ className="h-20 w-[26rem] resize-none"
416
+ placeholder="Type @ to mention someone…"
417
+ />
418
+ <MentionInputContent>
419
+ <MentionInputList>
420
+ {(option) => <MentionInputItem key={option.id} option={option} />}
421
+ </MentionInputList>
422
+ <MentionInputEmpty />
423
+ </MentionInputContent>
424
+ </MentionInput>
425
+ </div>
426
+ );
427
+ },
428
+ play: async ({ canvasElement, step }) => {
429
+ const field = within(canvasElement).getByRole("textbox", {
430
+ name: "Comment",
431
+ }) as HTMLTextAreaElement;
432
+ const overlay = canvasElement.querySelector<HTMLElement>('[data-slot="mention-input-overlay"]');
433
+ await expect(overlay).not.toBeNull();
434
+ await document.fonts.ready;
435
+
436
+ const boxBefore = field.getBoundingClientRect();
437
+
438
+ await step("the mirror starts in parity with the field", async () => {
439
+ await expect(getComputedStyle(overlay!).letterSpacing).toBe(
440
+ getComputedStyle(field).letterSpacing,
441
+ );
442
+ });
443
+
444
+ await step("change letter-spacing on the field's own style attribute", async () => {
445
+ field.style.letterSpacing = "3px";
446
+ await waitFor(() => {
447
+ expect(getComputedStyle(field).letterSpacing).toBe("3px");
448
+ });
449
+ });
450
+
451
+ await step("the box did NOT move", async () => {
452
+ const boxAfter = field.getBoundingClientRect();
453
+ await expect(boxAfter.width).toBeCloseTo(boxBefore.width, 1);
454
+ await expect(boxAfter.height).toBeCloseTo(boxBefore.height, 1);
455
+ });
456
+
457
+ await step("the mirror re-measured onto the new metrics", async () => {
458
+ await waitFor(() => {
459
+ expect(getComputedStyle(overlay!).letterSpacing).toBe("3px");
460
+ });
461
+ });
462
+ },
463
+ };