@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,202 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render } from "@testing-library/react";
3
+ import { MatchHighlight, queryToRanges, normalizeRanges } from "./match-highlight";
4
+
5
+ describe("MatchHighlight", () => {
6
+ it("renders one <mark> per case-insensitive query occurrence, text intact", () => {
7
+ const { container } = render(<MatchHighlight text="My Notes note" query="note" />);
8
+ const marks = container.querySelectorAll("mark");
9
+ expect(marks).toHaveLength(2);
10
+ expect(marks[0]?.textContent).toBe("Note");
11
+ expect(marks[1]?.textContent).toBe("note");
12
+ // Full string is preserved for screen readers.
13
+ expect(container.textContent).toBe("My Notes note");
14
+ });
15
+
16
+ it("merges adjacent precomputed ranges into a single <mark>", () => {
17
+ const { container } = render(
18
+ <MatchHighlight
19
+ text="abcdef"
20
+ ranges={[
21
+ [0, 2],
22
+ [2, 4],
23
+ ]}
24
+ />,
25
+ );
26
+ const marks = container.querySelectorAll("mark");
27
+ expect(marks).toHaveLength(1);
28
+ expect(marks[0]?.textContent).toBe("abcd"); // [0,4)
29
+ });
30
+
31
+ it("renders no <mark> for a non-matching query", () => {
32
+ const { container } = render(<MatchHighlight text="hello world" query="xyz" />);
33
+ expect(container.querySelectorAll("mark")).toHaveLength(0);
34
+ expect(container.textContent).toBe("hello world");
35
+ });
36
+
37
+ it("renders no <mark> for empty / whitespace-only-vs-nothing query", () => {
38
+ const { container } = render(<MatchHighlight text="hello" query="" />);
39
+ expect(container.querySelectorAll("mark")).toHaveLength(0);
40
+ expect(container.textContent).toBe("hello");
41
+ });
42
+
43
+ it("highlights multiple needles", () => {
44
+ const { container } = render(<MatchHighlight text="red green blue" query={["red", "blue"]} />);
45
+ const marks = container.querySelectorAll("mark");
46
+ expect(marks).toHaveLength(2);
47
+ expect(marks[0]?.textContent).toBe("red");
48
+ expect(marks[1]?.textContent).toBe("blue");
49
+ });
50
+
51
+ it("respects caseSensitive", () => {
52
+ const { container } = render(<MatchHighlight text="Note note" query="note" caseSensitive />);
53
+ const marks = container.querySelectorAll("mark");
54
+ expect(marks).toHaveLength(1);
55
+ expect(marks[0]?.textContent).toBe("note");
56
+ });
57
+
58
+ it("ranges win over query", () => {
59
+ const { container } = render(<MatchHighlight text="abcdef" query="ef" ranges={[[0, 1]]} />);
60
+ const marks = container.querySelectorAll("mark");
61
+ expect(marks).toHaveLength(1);
62
+ expect(marks[0]?.textContent).toBe("a");
63
+ });
64
+
65
+ it("handles matches at string start and end (non-adjacent stay separate)", () => {
66
+ const { container } = render(<MatchHighlight text="abc abc" query="abc" />);
67
+ const marks = container.querySelectorAll("mark");
68
+ expect(marks).toHaveLength(2);
69
+ expect(marks[0]?.textContent).toBe("abc"); // at index 0 (start)
70
+ expect(marks[1]?.textContent).toBe("abc"); // ending at text.length (end)
71
+ expect(container.textContent).toBe("abc abc");
72
+ });
73
+
74
+ it("merges two adjacent query matches into one contiguous mark (no seam)", () => {
75
+ const { container } = render(<MatchHighlight text="abcabc" query="abc" />);
76
+ const marks = container.querySelectorAll("mark");
77
+ expect(marks).toHaveLength(1);
78
+ expect(marks[0]?.textContent).toBe("abcabc");
79
+ });
80
+
81
+ it("handles unicode / accented needles without index drift", () => {
82
+ const { container } = render(<MatchHighlight text="café Café" query="café" />);
83
+ const marks = container.querySelectorAll("mark");
84
+ expect(marks).toHaveLength(2);
85
+ expect(container.textContent).toBe("café Café");
86
+ });
87
+
88
+ it("styles the mark with the semantic highlight token pair", () => {
89
+ const { container } = render(<MatchHighlight text="find me" query="me" />);
90
+ const mark = container.querySelector("mark");
91
+ expect(mark?.className).toContain("bg-highlight");
92
+ expect(mark?.className).toContain("text-highlight-foreground");
93
+ });
94
+
95
+ it("marks exactly one mark as current for activeIndex, and none by default", () => {
96
+ const { container, rerender } = render(<MatchHighlight text="abc abc abc" query="abc" />);
97
+ expect(container.querySelectorAll("mark[data-active]")).toHaveLength(0);
98
+ expect(container.querySelectorAll("mark[aria-current]")).toHaveLength(0);
99
+
100
+ rerender(<MatchHighlight text="abc abc abc" query="abc" activeIndex={1} />);
101
+ const active = container.querySelectorAll("mark[data-active]");
102
+ expect(active).toHaveLength(1);
103
+ expect(active[0]?.getAttribute("aria-current")).toBe("true");
104
+ // Second of the three, in document order.
105
+ expect(container.querySelectorAll("mark")[1]).toBe(active[0]);
106
+ });
107
+
108
+ it("gives the current mark its own token pair, distinct from the rest", () => {
109
+ const { container } = render(
110
+ <MatchHighlight text="abc abc" query="abc" activeIndex={0} data-testid="mh" />,
111
+ );
112
+ const [current, other] = Array.from(container.querySelectorAll("mark"));
113
+ expect(current?.className).toContain("bg-highlight-active");
114
+ expect(current?.className).toContain("text-highlight-active-foreground");
115
+ expect(other?.className).toContain("bg-highlight");
116
+ expect(other?.className).not.toContain("bg-highlight-active");
117
+ });
118
+
119
+ it("signals the current mark without relying on colour (WCAG 1.4.1)", () => {
120
+ const { container } = render(<MatchHighlight text="abc abc" query="abc" activeIndex={0} />);
121
+ const current = container.querySelector("mark[data-active]");
122
+ // An outline is a shape cue that survives greyscale; aria-current carries it
123
+ // to assistive tech, which never sees either colour.
124
+ expect(current?.className).toContain("outline-2");
125
+ expect(current?.getAttribute("aria-current")).toBe("true");
126
+ });
127
+
128
+ it("ignores an out-of-range activeIndex rather than clamping", () => {
129
+ const { container } = render(<MatchHighlight text="abc abc" query="abc" activeIndex={7} />);
130
+ expect(container.querySelectorAll("mark")).toHaveLength(2);
131
+ expect(container.querySelectorAll("mark[data-active]")).toHaveLength(0);
132
+ });
133
+
134
+ it("counts activeIndex against the MERGED ranges, not the raw ones", () => {
135
+ // [0,2) + [2,4) merge into one "abcd", so index 1 is the SECOND mark ("f").
136
+ const { container } = render(
137
+ <MatchHighlight
138
+ text="abcdef"
139
+ ranges={[
140
+ [0, 2],
141
+ [2, 4],
142
+ [5, 6],
143
+ ]}
144
+ activeIndex={1}
145
+ />,
146
+ );
147
+ const marks = container.querySelectorAll("mark");
148
+ expect(marks).toHaveLength(2);
149
+ expect(container.querySelector("mark[data-active]")?.textContent).toBe("f");
150
+ });
151
+
152
+ it("exposes stable data-slot selectors on the root and every mark", () => {
153
+ const { container } = render(<MatchHighlight text="abc abc" query="abc" />);
154
+ expect(container.querySelector('[data-slot="match-highlight"]')?.tagName).toBe("SPAN");
155
+ expect(container.querySelectorAll('[data-slot="match-highlight-mark"]')).toHaveLength(2);
156
+ });
157
+
158
+ it("merges className last and spreads props onto the span", () => {
159
+ const { container } = render(
160
+ <MatchHighlight text="x" query="x" className="custom-x" data-testid="mh" />,
161
+ );
162
+ const span = container.querySelector("span");
163
+ expect(span?.className).toContain("custom-x");
164
+ expect(span?.getAttribute("data-testid")).toBe("mh");
165
+ });
166
+ });
167
+
168
+ describe("range helpers", () => {
169
+ it("queryToRanges finds overlapping-needle matches", () => {
170
+ // "ab" [0,2) and "bc" [1,3) overlap; normalize should later merge.
171
+ expect(queryToRanges("abc", ["ab", "bc"])).toEqual([
172
+ [0, 2],
173
+ [1, 3],
174
+ ]);
175
+ });
176
+
177
+ it("normalizeRanges clamps, sorts, and merges overlaps + adjacency", () => {
178
+ expect(
179
+ normalizeRanges(
180
+ [
181
+ [1, 3],
182
+ [2, 5],
183
+ [5, 6],
184
+ ],
185
+ 10,
186
+ ),
187
+ ).toEqual([[1, 6]]);
188
+ expect(
189
+ normalizeRanges(
190
+ [
191
+ [-2, 2],
192
+ [8, 99],
193
+ ],
194
+ 10,
195
+ ),
196
+ ).toEqual([
197
+ [0, 2],
198
+ [8, 10],
199
+ ]);
200
+ expect(normalizeRanges([[4, 4]], 10)).toEqual([]); // empty range dropped
201
+ });
202
+ });
@@ -0,0 +1,145 @@
1
+ import { type HTMLAttributes, type ReactNode, forwardRef, Fragment } from "react";
2
+ import { cn } from "../../lib/cn";
3
+
4
+ /** A half-open `[start, end)` character range within the text. */
5
+ export type MatchRange = readonly [number, number];
6
+
7
+ export interface MatchHighlightProps extends HTMLAttributes<HTMLSpanElement> {
8
+ /** The full string to render. */
9
+ text: string;
10
+ /**
11
+ * One or more needles to highlight. Case-insensitive by default. Ignored when
12
+ * `ranges` is supplied.
13
+ */
14
+ query?: string | string[];
15
+ /**
16
+ * Precomputed match ranges (e.g. from a fuzzy matcher / MiniSearch). Wins over
17
+ * `query` when supplied. Overlapping and adjacent ranges are merged.
18
+ */
19
+ ranges?: readonly MatchRange[];
20
+ /** Match `query` case-sensitively. @default false */
21
+ caseSensitive?: boolean;
22
+ /**
23
+ * Which mark is the CURRENT one, 0-based among the resolved (merged, sorted)
24
+ * ranges. `-1` or omitted means none is.
25
+ *
26
+ * "One of twelve" and "the one you are on" are different meanings, so the
27
+ * current mark gets its own token pair rather than an opacity tweak — and,
28
+ * because colour is never the only channel (WCAG 1.4.1), an outline and
29
+ * `aria-current="true"` as well. It also carries `data-active`, which is how
30
+ * a caller finds it to scroll it into view:
31
+ * `root.querySelector('[data-slot="match-highlight-mark"][data-active]')`.
32
+ */
33
+ activeIndex?: number;
34
+ }
35
+
36
+ const escapeRegExp = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
37
+
38
+ /**
39
+ * Find every occurrence of each needle in `text` and return their `[start, end)`
40
+ * ranges. Uses a per-needle global regex so indices land on the ORIGINAL string
41
+ * (unicode-safe — no `toLowerCase()` length drift), handling multiple needles and
42
+ * case folding. Empty needles are ignored.
43
+ */
44
+ export function queryToRanges(
45
+ text: string,
46
+ query: string | string[],
47
+ caseSensitive = false,
48
+ ): MatchRange[] {
49
+ const needles = (Array.isArray(query) ? query : [query]).filter((q) => q.length > 0);
50
+ if (needles.length === 0 || text.length === 0) return [];
51
+ const flags = caseSensitive ? "gu" : "giu";
52
+ const out: MatchRange[] = [];
53
+ for (const needle of needles) {
54
+ let re: RegExp;
55
+ try {
56
+ re = new RegExp(escapeRegExp(needle), flags);
57
+ } catch {
58
+ continue; // pathological needle — skip rather than throw
59
+ }
60
+ for (const m of text.matchAll(re)) {
61
+ if (m.index === undefined) continue;
62
+ const end = m.index + m[0].length;
63
+ if (end > m.index) out.push([m.index, end]);
64
+ }
65
+ }
66
+ return out;
67
+ }
68
+
69
+ /**
70
+ * Clamp ranges to `[0, len)`, drop empty/invalid ones, sort, and merge
71
+ * overlapping AND adjacent ranges (so `[0,2]` + `[2,4]` → a single `[0,4]`).
72
+ */
73
+ export function normalizeRanges(ranges: readonly MatchRange[], len: number): MatchRange[] {
74
+ const clamped = ranges
75
+ .map(([a, b]): MatchRange => [Math.max(0, Math.min(a, b)), Math.min(len, Math.max(a, b))])
76
+ .filter(([a, b]) => a < b);
77
+ clamped.sort((x, y) => x[0] - y[0] || x[1] - y[1]);
78
+ const merged: [number, number][] = [];
79
+ for (const [a, b] of clamped) {
80
+ const last = merged[merged.length - 1];
81
+ if (last && a <= last[1]) {
82
+ last[1] = Math.max(last[1], b);
83
+ } else {
84
+ merged.push([a, b]);
85
+ }
86
+ }
87
+ return merged;
88
+ }
89
+
90
+ /**
91
+ * Render `text` with the query matches wrapped in real `<mark>` elements. Purely
92
+ * presentational: pass `query` to let it compute ranges (case-insensitive by
93
+ * default), or `ranges` from your own fuzzy engine. Marks use the semantic
94
+ * `--highlight` token pair (AA-legible ink in every theme). Screen readers read
95
+ * the full string naturally.
96
+ */
97
+ export const MatchHighlight = forwardRef<HTMLSpanElement, MatchHighlightProps>(
98
+ function MatchHighlight(
99
+ { text, query, ranges, caseSensitive = false, activeIndex = -1, className, ...props },
100
+ ref,
101
+ ) {
102
+ const resolved = normalizeRanges(
103
+ ranges ?? (query != null ? queryToRanges(text, query, caseSensitive) : []),
104
+ text.length,
105
+ );
106
+
107
+ const segments: ReactNode[] = [];
108
+ let cursor = 0;
109
+ resolved.forEach(([start, end], index) => {
110
+ if (start > cursor) {
111
+ segments.push(<Fragment key={`t${cursor}`}>{text.slice(cursor, start)}</Fragment>);
112
+ }
113
+ const active = index === activeIndex;
114
+ segments.push(
115
+ <mark
116
+ key={`m${start}`}
117
+ data-slot="match-highlight-mark"
118
+ // Present-or-absent, so the current mark is still identifiable in
119
+ // greyscale and to a screen reader — the colour swap below is the
120
+ // enhancement, not the signal (WCAG 1.4.1).
121
+ data-active={active ? "" : undefined}
122
+ aria-current={active ? "true" : undefined}
123
+ className={cn(
124
+ "rounded-sm px-0.5 font-medium",
125
+ active
126
+ ? "outline-foreground bg-highlight-active text-highlight-active-foreground outline-2 outline-offset-1"
127
+ : "bg-highlight text-highlight-foreground",
128
+ )}
129
+ >
130
+ {text.slice(start, end)}
131
+ </mark>,
132
+ );
133
+ cursor = end;
134
+ });
135
+ if (cursor < text.length) {
136
+ segments.push(<Fragment key={`t${cursor}`}>{text.slice(cursor)}</Fragment>);
137
+ }
138
+
139
+ return (
140
+ <span ref={ref} data-slot="match-highlight" className={cn(className)} {...props}>
141
+ {segments}
142
+ </span>
143
+ );
144
+ },
145
+ );
@@ -0,0 +1,28 @@
1
+ export {
2
+ MentionInput,
3
+ MentionInputContent,
4
+ MentionInputEmpty,
5
+ MentionInputItem,
6
+ MentionInputList,
7
+ MentionInputTextarea,
8
+ useMentionInput,
9
+ type MentionInputContentProps,
10
+ type MentionInputContextValue,
11
+ type MentionInputEmptyProps,
12
+ type MentionInputItemProps,
13
+ type MentionInputListProps,
14
+ type MentionInputProps,
15
+ type MentionInputTextareaProps,
16
+ } from "./mention-input";
17
+ export {
18
+ defaultMentionFilter,
19
+ insertMention,
20
+ mentionAt,
21
+ mentionEnd,
22
+ remapMentions,
23
+ serializeMentions,
24
+ type InsertMentionResult,
25
+ type Mention,
26
+ type MentionOption,
27
+ type MentionValue,
28
+ } from "./mention-value";
@@ -0,0 +1,171 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, waitFor, within } from "storybook/test";
3
+ import { useId, type CSSProperties } 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 type { MentionOption, MentionValue } from "./mention-value";
14
+
15
+ /**
16
+ * The `MutationObserver` arm of the mirror re-measure, pinned on its own.
17
+ *
18
+ * ### Why this lives in its own file
19
+ *
20
+ * The lock is **order-dependent**, and that was measured, not guessed. With this
21
+ * story co-resident with the other `MentionInput` stories, deleting the
22
+ * `MutationObserver` left the whole suite green; running the same story in
23
+ * isolation, the same deletion is RED 2/2. Something a preceding story leaves
24
+ * behind in the shared page re-measures the mirror for an unrelated reason. A
25
+ * lock that only holds when it runs alone is not a lock at all in CI — so it
26
+ * gets its own story file, which the runner loads as its own test file.
27
+ *
28
+ * ### What it pins
29
+ *
30
+ * Every box-affecting property of the field is nailed to absolute pixels, so
31
+ * `ResizeObserver` is structurally unable to fire, and `document.fonts.ready` is
32
+ * awaited before the flip so that arm cannot fire late. `h-20 w-[26rem]` was NOT
33
+ * enough: padding and border derived from `rem` can move by sub-integer amounts
34
+ * across a theme change, which `clientWidth`'s integer rounding hides but
35
+ * `ResizeObserver` still reports.
36
+ *
37
+ * The play function asserts the box did not move — fractionally via
38
+ * `getBoundingClientRect` as well as in integers — so the isolation is proven in
39
+ * the test rather than argued for in a comment.
40
+ */
41
+ const meta = {
42
+ title: "Forms/MentionInput/Mirror re-measure",
43
+ component: MentionInput,
44
+ parameters: {
45
+ docs: {
46
+ description: {
47
+ component:
48
+ "Regression lock for the chip mirror's re-measure. Isolated in its own " +
49
+ "story file so no sibling story can mask the arm it pins.",
50
+ },
51
+ },
52
+ },
53
+ } satisfies Meta<typeof MentionInput>;
54
+
55
+ export default meta;
56
+ type Story = StoryObj<typeof meta>;
57
+
58
+ const ROSTER: MentionOption[] = [
59
+ { id: "u-lovelace", label: "Ada Lovelace", description: "Analytical Engine" },
60
+ { id: "u-hopper", label: "Grace Hopper", description: "Compilers" },
61
+ ];
62
+
63
+ const MIRROR_VALUE: MentionValue = {
64
+ text: "@Ada Lovelace ships",
65
+ mentions: [{ id: "u-lovelace", label: "Ada Lovelace", start: 0 }],
66
+ };
67
+
68
+ /** Every degree of freedom the field's box has, removed. */
69
+ const NAILED_BOX: CSSProperties = {
70
+ boxSizing: "border-box",
71
+ width: "420px",
72
+ height: "80px",
73
+ paddingTop: "8px",
74
+ paddingRight: "12px",
75
+ paddingBottom: "8px",
76
+ paddingLeft: "12px",
77
+ borderWidth: "1px",
78
+ lineHeight: "20px",
79
+ fontSize: "14px",
80
+ overflow: "hidden",
81
+ resize: "none",
82
+ };
83
+
84
+ /** The element the theme decorator actually wrote `data-theme` onto. */
85
+ function themeHost(): HTMLElement {
86
+ return document.body.hasAttribute("data-theme") ? document.body : document.documentElement;
87
+ }
88
+
89
+ /**
90
+ * **Regression lock — the `MutationObserver` arm, individually pinned.**
91
+ *
92
+ * An in-place `data-theme` flip, exactly what `ThemeSwitcher` does (no remount).
93
+ * Deleting the `MutationObserver` block turns this RED with the original bug's
94
+ * symptom verbatim: the overlay stuck on the `:root` fallback `Inter` while the
95
+ * field renders `IBM Plex Mono`.
96
+ */
97
+ export const TracksThemeChangeWithNailedBox: Story = {
98
+ args: { options: ROSTER, children: null },
99
+ render: function MirrorNailedBoxStory() {
100
+ const id = useId();
101
+ return (
102
+ <div className="space-y-2">
103
+ <Label htmlFor={id}>Comment</Label>
104
+ <MentionInput options={ROSTER} defaultValue={MIRROR_VALUE}>
105
+ <MentionInputTextarea id={id} style={NAILED_BOX} placeholder="Type @ to mention…" />
106
+ <MentionInputContent>
107
+ <MentionInputList>
108
+ {(option) => <MentionInputItem key={option.id} option={option} />}
109
+ </MentionInputList>
110
+ <MentionInputEmpty />
111
+ </MentionInputContent>
112
+ </MentionInput>
113
+ </div>
114
+ );
115
+ },
116
+ play: async ({ canvasElement, step }) => {
117
+ const field = within(canvasElement).getByRole("textbox", {
118
+ name: "Comment",
119
+ }) as HTMLTextAreaElement;
120
+ const overlay = canvasElement.querySelector<HTMLElement>('[data-slot="mention-input-overlay"]');
121
+ await expect(overlay).not.toBeNull();
122
+
123
+ // Settle the fonts arm before touching anything, so it cannot fire late.
124
+ await document.fonts.ready;
125
+
126
+ const boxOf = (el: HTMLElement) => {
127
+ const r = el.getBoundingClientRect();
128
+ return { w: r.width, h: r.height, cw: el.clientWidth, ch: el.clientHeight };
129
+ };
130
+ const before = boxOf(field);
131
+ const host = themeHost();
132
+ const original = host.getAttribute("data-theme");
133
+ const fontBefore = getComputedStyle(field).fontFamily;
134
+
135
+ try {
136
+ await step("the mirror starts in parity with the field", async () => {
137
+ await expect(getComputedStyle(overlay!).fontFamily).toBe(fontBefore);
138
+ });
139
+
140
+ await step("flip data-theme in place", async () => {
141
+ // The shipped themes share one font stack, so the attribute flip alone
142
+ // changes nothing measurable. Pair it with a scoped `--font-sans`
143
+ // override (the same root `style` the observer also watches) so the
144
+ // parity assertion below cannot pass vacuously.
145
+ host.style.setProperty("--font-sans", '"Times New Roman", serif');
146
+ host.setAttribute("data-theme", original === "dark" ? "light" : "dark");
147
+ await waitFor(() => {
148
+ expect(getComputedStyle(field).fontFamily).not.toBe(fontBefore);
149
+ });
150
+ });
151
+
152
+ await step("the box did not move — ResizeObserver cannot have fired", async () => {
153
+ const after = boxOf(field);
154
+ await expect(after.w).toBe(before.w);
155
+ await expect(after.h).toBe(before.h);
156
+ await expect(after.cw).toBe(before.cw);
157
+ await expect(after.ch).toBe(before.ch);
158
+ });
159
+
160
+ await step("the mirror re-measured — only the MutationObserver could have", async () => {
161
+ await waitFor(() => {
162
+ expect(getComputedStyle(overlay!).fontFamily).toBe(getComputedStyle(field).fontFamily);
163
+ });
164
+ });
165
+ } finally {
166
+ host.style.removeProperty("--font-sans");
167
+ if (original === null) host.removeAttribute("data-theme");
168
+ else host.setAttribute("data-theme", original);
169
+ }
170
+ },
171
+ };