@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,253 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ defaultMentionFilter,
4
+ insertMention,
5
+ mentionAt,
6
+ mentionEnd,
7
+ remapMentions,
8
+ serializeMentions,
9
+ type MentionOption,
10
+ type MentionValue,
11
+ } from "./mention-value";
12
+
13
+ const ada: MentionOption = { id: "u1", label: "Ada Lovelace", keywords: ["ada", "math"] };
14
+ const grace: MentionOption = { id: "u2", label: "Grace Hopper" };
15
+
16
+ /** "Hi @Ada Lovelace how are you" with the mention recorded at offset 3. */
17
+ const oneMention: MentionValue = {
18
+ text: "Hi @Ada Lovelace how are you",
19
+ mentions: [{ id: "u1", label: "Ada Lovelace", start: 3 }],
20
+ };
21
+
22
+ /** "@Ada Lovelace and @Grace Hopper ship" — two mentions, at 0 and 18. */
23
+ const twoMentions: MentionValue = {
24
+ text: "@Ada Lovelace and @Grace Hopper ship",
25
+ mentions: [
26
+ { id: "u1", label: "Ada Lovelace", start: 0 },
27
+ { id: "u2", label: "Grace Hopper", start: 18 },
28
+ ],
29
+ };
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // T1 — remapMentions offset algebra
33
+ // ---------------------------------------------------------------------------
34
+
35
+ describe("T1 remapMentions — offset algebra", () => {
36
+ it("is idempotent when the text is unchanged", () => {
37
+ expect(remapMentions(oneMention, oneMention.text)).toEqual(oneMention.mentions);
38
+ });
39
+
40
+ it("shifts a mention when text is inserted BEFORE it", () => {
41
+ // "Hi " -> "Oh Hi " (3 chars inserted at 0)
42
+ const next = "Oh Hi @Ada Lovelace how are you";
43
+ expect(remapMentions(oneMention, next)).toEqual([
44
+ { id: "u1", label: "Ada Lovelace", start: 6 },
45
+ ]);
46
+ });
47
+
48
+ it("holds the offset when text is inserted AFTER it", () => {
49
+ const next = "Hi @Ada Lovelace how are you today";
50
+ expect(remapMentions(oneMention, next)).toEqual([
51
+ { id: "u1", label: "Ada Lovelace", start: 3 },
52
+ ]);
53
+ });
54
+
55
+ it("holds the first and shifts the second when text is inserted BETWEEN them", () => {
56
+ const next = "@Ada Lovelace and also @Grace Hopper ship";
57
+ expect(remapMentions(twoMentions, next)).toEqual([
58
+ { id: "u1", label: "Ada Lovelace", start: 0 },
59
+ { id: "u2", label: "Grace Hopper", start: 23 },
60
+ ]);
61
+ });
62
+
63
+ it("shifts every mention when text is removed before both", () => {
64
+ // Drop the leading "Hi " from a two-mention value.
65
+ const prev: MentionValue = {
66
+ text: "Hi @Ada Lovelace and @Grace Hopper",
67
+ mentions: [
68
+ { id: "u1", label: "Ada Lovelace", start: 3 },
69
+ { id: "u2", label: "Grace Hopper", start: 21 },
70
+ ],
71
+ };
72
+ expect(remapMentions(prev, "@Ada Lovelace and @Grace Hopper")).toEqual([
73
+ { id: "u1", label: "Ada Lovelace", start: 0 },
74
+ { id: "u2", label: "Grace Hopper", start: 18 },
75
+ ]);
76
+ });
77
+
78
+ it("DROPS a mention when the edit intersects its range", () => {
79
+ // Delete one character out of the middle of "@Ada Lovelace".
80
+ const next = "Hi @Ada Loveace how are you";
81
+ expect(remapMentions(oneMention, next)).toEqual([]);
82
+ });
83
+
84
+ it("DROPS a mention when a character is inserted inside its range", () => {
85
+ const next = "Hi @Adda Lovelace how are you";
86
+ expect(remapMentions(oneMention, next)).toEqual([]);
87
+ });
88
+
89
+ it("keeps a mention when a character is inserted flush against its end", () => {
90
+ const next = "Hi @Ada Lovelace! how are you";
91
+ expect(remapMentions(oneMention, next)).toEqual([
92
+ { id: "u1", label: "Ada Lovelace", start: 3 },
93
+ ]);
94
+ });
95
+
96
+ it("survives a multi-mention paste that replaces a middle span", () => {
97
+ // Replace " and " (between the two mentions) with " plus ".
98
+ const next = "@Ada Lovelace plus @Grace Hopper ship";
99
+ expect(remapMentions(twoMentions, next)).toEqual([
100
+ { id: "u1", label: "Ada Lovelace", start: 0 },
101
+ { id: "u2", label: "Grace Hopper", start: 19 },
102
+ ]);
103
+ });
104
+
105
+ it("drops every mention when the whole text is replaced", () => {
106
+ expect(remapMentions(twoMentions, "totally different text")).toEqual([]);
107
+ });
108
+
109
+ it("re-validates survivors against the new text (a shifted offset that no longer spells the token is dropped)", () => {
110
+ // A hand-crafted value whose recorded offset is a lie: re-validation must
111
+ // reject it rather than trusting the arithmetic.
112
+ const lying: MentionValue = {
113
+ text: "Hi @Ada Lovelace",
114
+ mentions: [{ id: "u1", label: "Grace Hopper", start: 3 }],
115
+ };
116
+ expect(remapMentions(lying, "Hi! @Ada Lovelace")).toEqual([]);
117
+ });
118
+
119
+ it("honours a non-default trigger character", () => {
120
+ const hash: MentionValue = {
121
+ text: "see #Roadmap now",
122
+ mentions: [{ id: "t1", label: "Roadmap", start: 4 }],
123
+ };
124
+ expect(remapMentions(hash, "please see #Roadmap now", "#")).toEqual([
125
+ { id: "t1", label: "Roadmap", start: 11 },
126
+ ]);
127
+ });
128
+ });
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // T2 — serializeMentions shape
132
+ // ---------------------------------------------------------------------------
133
+
134
+ describe("T2 serializeMentions — the consumer contract", () => {
135
+ it("returns { text, mentionedIds }", () => {
136
+ expect(serializeMentions(twoMentions)).toEqual({
137
+ text: "@Ada Lovelace and @Grace Hopper ship",
138
+ mentionedIds: ["u1", "u2"],
139
+ });
140
+ });
141
+
142
+ it("returns an empty id list for a mention-free value", () => {
143
+ expect(serializeMentions({ text: "just words", mentions: [] })).toEqual({
144
+ text: "just words",
145
+ mentionedIds: [],
146
+ });
147
+ });
148
+
149
+ it("dedupes ids — two mentions of the same person yield one id", () => {
150
+ const twice: MentionValue = {
151
+ text: "@Ada Lovelace and @Ada Lovelace",
152
+ mentions: [
153
+ { id: "u1", label: "Ada Lovelace", start: 0 },
154
+ { id: "u1", label: "Ada Lovelace", start: 18 },
155
+ ],
156
+ };
157
+ expect(serializeMentions(twice).mentionedIds).toEqual(["u1"]);
158
+ });
159
+
160
+ it("emits ids in DOCUMENT order, not insertion order", () => {
161
+ const outOfOrder: MentionValue = {
162
+ text: "@Ada Lovelace and @Grace Hopper",
163
+ mentions: [
164
+ { id: "u2", label: "Grace Hopper", start: 18 },
165
+ { id: "u1", label: "Ada Lovelace", start: 0 },
166
+ ],
167
+ };
168
+ expect(serializeMentions(outOfOrder).mentionedIds).toEqual(["u1", "u2"]);
169
+ });
170
+ });
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // insertMention
174
+ // ---------------------------------------------------------------------------
175
+
176
+ describe("insertMention", () => {
177
+ it("replaces trigger+query with trigger+label+space and reports the caret after the space", () => {
178
+ const value: MentionValue = { text: "Hi @ad", mentions: [] };
179
+ const result = insertMention(value, ada, "@", 3, 6);
180
+ expect(result.value.text).toBe("Hi @Ada Lovelace ");
181
+ expect(result.value.mentions).toEqual([{ id: "u1", label: "Ada Lovelace", start: 3 }]);
182
+ expect(result.caret).toBe(17);
183
+ });
184
+
185
+ it("keeps text that follows the caret and shifts later mentions", () => {
186
+ const value: MentionValue = {
187
+ text: "@gr ping @Ada Lovelace",
188
+ mentions: [{ id: "u1", label: "Ada Lovelace", start: 9 }],
189
+ };
190
+ const result = insertMention(value, grace, "@", 0, 3);
191
+ expect(result.value.text).toBe("@Grace Hopper ping @Ada Lovelace");
192
+ expect(result.value.mentions).toEqual([
193
+ { id: "u2", label: "Grace Hopper", start: 0 },
194
+ { id: "u1", label: "Ada Lovelace", start: 20 },
195
+ ]);
196
+ });
197
+
198
+ it("records mentions sorted by document order", () => {
199
+ const value: MentionValue = {
200
+ text: "@Grace Hopper @ad",
201
+ mentions: [{ id: "u2", label: "Grace Hopper", start: 0 }],
202
+ };
203
+ const result = insertMention(value, ada, "@", 14, 17);
204
+ expect(result.value.mentions.map((m) => m.id)).toEqual(["u2", "u1"]);
205
+ });
206
+ });
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // mentionAt / mentionEnd — the basis for chip atomicity
210
+ // ---------------------------------------------------------------------------
211
+
212
+ describe("mentionAt / mentionEnd", () => {
213
+ it("mentionEnd covers trigger + label", () => {
214
+ expect(mentionEnd(oneMention.mentions[0]!, "@")).toBe(16);
215
+ });
216
+
217
+ it("finds the mention containing an index inside its range", () => {
218
+ expect(mentionAt(oneMention, 3)?.id).toBe("u1");
219
+ expect(mentionAt(oneMention, 10)?.id).toBe("u1");
220
+ expect(mentionAt(oneMention, 15)?.id).toBe("u1");
221
+ });
222
+
223
+ it("treats the range as half-open — the end offset is OUTSIDE", () => {
224
+ expect(mentionAt(oneMention, 16)).toBeUndefined();
225
+ expect(mentionAt(oneMention, 2)).toBeUndefined();
226
+ });
227
+
228
+ it("returns undefined for a mention-free value", () => {
229
+ expect(mentionAt({ text: "nothing here", mentions: [] }, 4)).toBeUndefined();
230
+ });
231
+ });
232
+
233
+ // ---------------------------------------------------------------------------
234
+ // defaultMentionFilter
235
+ // ---------------------------------------------------------------------------
236
+
237
+ describe("defaultMentionFilter", () => {
238
+ it("matches everything on an empty query", () => {
239
+ expect(defaultMentionFilter(ada, "")).toBe(true);
240
+ expect(defaultMentionFilter(grace, "")).toBe(true);
241
+ });
242
+
243
+ it("matches the label case-insensitively, as a substring", () => {
244
+ expect(defaultMentionFilter(ada, "love")).toBe(true);
245
+ expect(defaultMentionFilter(ada, "ADA")).toBe(true);
246
+ expect(defaultMentionFilter(ada, "zz")).toBe(false);
247
+ });
248
+
249
+ it("matches extra keywords", () => {
250
+ expect(defaultMentionFilter(ada, "math")).toBe(true);
251
+ expect(defaultMentionFilter(grace, "math")).toBe(false);
252
+ });
253
+ });
@@ -0,0 +1,193 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ /**
4
+ * The pure offset algebra behind `MentionInput`.
5
+ *
6
+ * Chip atomicity in a real `<textarea>` is **selection arithmetic over a token
7
+ * map**, not DOM nodes: the field holds ordinary text, and this module keeps a
8
+ * parallel list of `{ id, label, start }` records honest as that text is edited,
9
+ * pasted over and deleted. Nothing here touches React or the DOM, so every
10
+ * acceptance criterion about backspace atomicity, caret skipping and the
11
+ * `{ text, mentionedIds }` payload reduces to a unit test on these functions.
12
+ */
13
+
14
+ /** One selectable roster entry. */
15
+ export interface MentionOption {
16
+ /** Stable id — this is what `serializeMentions` emits. */
17
+ id: string;
18
+ /** Inserted verbatim after the trigger character. */
19
+ label: string;
20
+ /** Optional secondary line shown under the label. */
21
+ description?: string;
22
+ /** Optional leading glyph (avatar, icon). */
23
+ icon?: ReactNode;
24
+ /** Extra match terms, so "ada" can find "Ada Lovelace". */
25
+ keywords?: string[];
26
+ /** Rendered but not selectable. */
27
+ disabled?: boolean;
28
+ }
29
+
30
+ /** A resolved mention inside `MentionValue.text`. */
31
+ export interface Mention {
32
+ /** The option id this run refers to. */
33
+ id: string;
34
+ /** The label WITHOUT the trigger character. */
35
+ label: string;
36
+ /** Offset of the trigger character in `text`. */
37
+ start: number;
38
+ }
39
+
40
+ /** The component's value: plain text plus the mentions it contains. */
41
+ export interface MentionValue {
42
+ text: string;
43
+ mentions: Mention[];
44
+ }
45
+
46
+ /** The result of inserting an option: the new value and where the caret goes. */
47
+ export interface InsertMentionResult {
48
+ value: MentionValue;
49
+ /** Offset immediately after the inserted token's trailing space. */
50
+ caret: number;
51
+ }
52
+
53
+ /** Exclusive end offset of a mention's `trigger + label` run. */
54
+ export function mentionEnd(mention: Mention, trigger = "@"): number {
55
+ return mention.start + trigger.length + mention.label.length;
56
+ }
57
+
58
+ /**
59
+ * The mention whose half-open range `[start, end)` contains `index`, if any.
60
+ *
61
+ * Half-open on purpose: the offset one past the token is *outside* it, so a
62
+ * caret sitting immediately after `@Ada Lovelace` is "next to" the mention
63
+ * rather than "in" it. Backspace therefore asks about `caret - 1` and Delete
64
+ * about `caret`, and both get an unambiguous answer.
65
+ */
66
+ export function mentionAt(value: MentionValue, index: number, trigger = "@"): Mention | undefined {
67
+ return value.mentions.find((m) => index >= m.start && index < mentionEnd(m, trigger));
68
+ }
69
+
70
+ /** Length of the longest common prefix of two strings. */
71
+ function commonPrefixLength(a: string, b: string): number {
72
+ const max = Math.min(a.length, b.length);
73
+ let i = 0;
74
+ while (i < max && a[i] === b[i]) i++;
75
+ return i;
76
+ }
77
+
78
+ /**
79
+ * Re-derive the mention list after the text changed underneath it.
80
+ *
81
+ * The edit is treated as ONE contiguous replacement — which is what a
82
+ * keystroke, a paste, a cut and a native undo all are. The common prefix and
83
+ * common suffix bracket it; a mention entirely before the edit keeps its
84
+ * offset, one entirely after it shifts by the length delta, and one the edit
85
+ * reaches into is **dropped** (its text no longer spells the person's name, so
86
+ * the id would be a lie).
87
+ *
88
+ * Every survivor is then **re-validated** against the new text: the run at its
89
+ * offset must still read exactly `trigger + label`. That second pass is what
90
+ * makes a multi-mention paste safe — the arithmetic alone can be fooled by a
91
+ * replacement whose prefix/suffix boundaries land ambiguously, but a run that
92
+ * no longer spells the token can never survive the check.
93
+ */
94
+ export function remapMentions(prev: MentionValue, nextText: string, trigger = "@"): Mention[] {
95
+ if (prev.text === nextText) return prev.mentions;
96
+
97
+ const prevText = prev.text;
98
+ const p = commonPrefixLength(prevText, nextText);
99
+
100
+ const maxSuffix = Math.min(prevText.length, nextText.length) - p;
101
+ let s = 0;
102
+ while (s < maxSuffix && prevText[prevText.length - 1 - s] === nextText[nextText.length - 1 - s]) {
103
+ s++;
104
+ }
105
+
106
+ /** End of the replaced span, in PREVIOUS coordinates. */
107
+ const editEnd = prevText.length - s;
108
+ const delta = nextText.length - prevText.length;
109
+
110
+ const remapped: Mention[] = [];
111
+ for (const mention of prev.mentions) {
112
+ const end = mentionEnd(mention, trigger);
113
+ if (end <= p) {
114
+ remapped.push(mention);
115
+ } else if (mention.start >= editEnd) {
116
+ remapped.push({ ...mention, start: mention.start + delta });
117
+ }
118
+ // else: the edit reached into the token — drop it.
119
+ }
120
+
121
+ return remapped.filter(
122
+ (m) => nextText.slice(m.start, mentionEnd(m, trigger)) === trigger + m.label,
123
+ );
124
+ }
125
+
126
+ /**
127
+ * Replace the in-progress `trigger + query` run with `trigger + label + " "`.
128
+ *
129
+ * `queryStart` is the offset of the trigger character; `caret` is where the
130
+ * query ends. The trailing space is deliberate: it terminates the query so the
131
+ * popup closes, and it gives the next word somewhere to start.
132
+ */
133
+ export function insertMention(
134
+ value: MentionValue,
135
+ option: MentionOption,
136
+ trigger: string,
137
+ queryStart: number,
138
+ caret: number,
139
+ ): InsertMentionResult {
140
+ const inserted = `${trigger}${option.label} `;
141
+ const nextText = value.text.slice(0, queryStart) + inserted + value.text.slice(caret);
142
+ const delta = inserted.length - (caret - queryStart);
143
+
144
+ const mentions: Mention[] = [];
145
+ for (const mention of value.mentions) {
146
+ if (mentionEnd(mention, trigger) <= queryStart) {
147
+ mentions.push(mention);
148
+ } else if (mention.start >= caret) {
149
+ mentions.push({ ...mention, start: mention.start + delta });
150
+ }
151
+ // else: it overlapped the query run being replaced — drop it.
152
+ }
153
+ mentions.push({ id: option.id, label: option.label, start: queryStart });
154
+ mentions.sort((a, b) => a.start - b.start);
155
+
156
+ return {
157
+ value: {
158
+ text: nextText,
159
+ mentions: mentions.filter(
160
+ (m) => nextText.slice(m.start, mentionEnd(m, trigger)) === trigger + m.label,
161
+ ),
162
+ },
163
+ caret: queryStart + inserted.length,
164
+ };
165
+ }
166
+
167
+ /**
168
+ * The serialization contract a consumer gets out: the raw text plus the option
169
+ * ids it references, **deduped** and in **document order** (two mentions of the
170
+ * same person yield one id).
171
+ */
172
+ export function serializeMentions(value: MentionValue): { text: string; mentionedIds: string[] } {
173
+ const ordered = [...value.mentions].sort((a, b) => a.start - b.start);
174
+ const mentionedIds: string[] = [];
175
+ const seen = new Set<string>();
176
+ for (const mention of ordered) {
177
+ if (seen.has(mention.id)) continue;
178
+ seen.add(mention.id);
179
+ mentionedIds.push(mention.id);
180
+ }
181
+ return { text: value.text, mentionedIds };
182
+ }
183
+
184
+ /**
185
+ * Case-insensitive substring match over `label` and `keywords`. An empty query
186
+ * matches everything, so the full roster shows the moment the trigger is typed.
187
+ */
188
+ export function defaultMentionFilter(option: MentionOption, query: string): boolean {
189
+ const q = query.trim().toLowerCase();
190
+ if (q.length === 0) return true;
191
+ if (option.label.toLowerCase().includes(q)) return true;
192
+ return (option.keywords ?? []).some((k) => k.toLowerCase().includes(q));
193
+ }
@@ -0,0 +1,17 @@
1
+ export {
2
+ Menubar,
3
+ MenubarMenu,
4
+ MenubarGroup,
5
+ MenubarPortal,
6
+ MenubarSub,
7
+ MenubarRadioGroup,
8
+ MenubarTrigger,
9
+ MenubarContent,
10
+ MenubarItem,
11
+ MenubarSubTrigger,
12
+ MenubarSubContent,
13
+ MenubarCheckboxItem,
14
+ MenubarRadioItem,
15
+ MenubarSeparator,
16
+ MenubarShortcut,
17
+ } from "./menubar";
@@ -0,0 +1,67 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, waitFor, within } from "storybook/test";
3
+ import {
4
+ Menubar,
5
+ MenubarContent,
6
+ MenubarItem,
7
+ MenubarMenu,
8
+ MenubarSeparator,
9
+ MenubarShortcut,
10
+ MenubarTrigger,
11
+ } from "./menubar";
12
+ const meta = {
13
+ title: "Navigation/Menubar",
14
+ component: Menubar,
15
+ tags: ["autodocs"],
16
+ argTypes: {
17
+ loop: {
18
+ description: "Whether keyboard navigation wraps from the last menu back to the first.",
19
+ control: "boolean",
20
+ table: { category: "Behaviour" },
21
+ },
22
+ dir: {
23
+ description: "Reading direction for the menubar.",
24
+ control: { type: "select" },
25
+ options: ["ltr", "rtl"],
26
+ table: { category: "Behaviour" },
27
+ },
28
+ className: {
29
+ description: "Additional CSS classes applied to the menubar root.",
30
+ control: "text",
31
+ table: { category: "Styling" },
32
+ },
33
+ },
34
+ } satisfies Meta<typeof Menubar>;
35
+ export default meta;
36
+ type Story = StoryObj<typeof meta>;
37
+ export const Default: Story = {
38
+ render: () => (
39
+ <Menubar>
40
+ <MenubarMenu>
41
+ <MenubarTrigger>File</MenubarTrigger>
42
+ <MenubarContent>
43
+ <MenubarItem>
44
+ New <MenubarShortcut>⌘N</MenubarShortcut>
45
+ </MenubarItem>
46
+ <MenubarItem>Open</MenubarItem>
47
+ <MenubarSeparator />
48
+ <MenubarItem>Quit</MenubarItem>
49
+ </MenubarContent>
50
+ </MenubarMenu>
51
+ </Menubar>
52
+ ),
53
+ // Clicks the "File" trigger to open the portalled menubar content and
54
+ // confirms the menu items are visible, then presses Escape to close it.
55
+ play: async ({ canvas, canvasElement, userEvent }) => {
56
+ await userEvent.click(canvas.getByRole("menuitem", { name: /file/i }));
57
+ const body = within(canvasElement.ownerDocument.body);
58
+ const menu = await body.findByRole("menu");
59
+ await waitFor(() =>
60
+ expect(within(menu).getByRole("menuitem", { name: /open/i })).toBeVisible(),
61
+ );
62
+ await waitFor(() =>
63
+ expect(within(menu).getByRole("menuitem", { name: /quit/i })).toBeVisible(),
64
+ );
65
+ await userEvent.keyboard("{Escape}");
66
+ },
67
+ };