@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,96 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { ListEditor } from "./list-editor";
5
+
6
+ describe("ListEditor", () => {
7
+ it("renders one input per row", () => {
8
+ render(<ListEditor defaultValue={["a", "b", "c"]} />);
9
+ expect(screen.getAllByRole("textbox")).toHaveLength(3);
10
+ });
11
+
12
+ it("renders a real empty state for an empty list", () => {
13
+ render(<ListEditor defaultValue={[]} />);
14
+ expect(screen.getByText("No items yet.")).toBeInTheDocument();
15
+ expect(screen.queryByRole("textbox")).not.toBeInTheDocument();
16
+ });
17
+
18
+ it("adds a row via the Add item button", async () => {
19
+ const onValueChange = vi.fn();
20
+ render(<ListEditor defaultValue={["a"]} onValueChange={onValueChange} />);
21
+ await userEvent.click(screen.getByRole("button", { name: /add item/i }));
22
+ expect(onValueChange).toHaveBeenCalledWith(["a", ""]);
23
+ });
24
+
25
+ it("edits a row's value", async () => {
26
+ const onValueChange = vi.fn();
27
+ render(<ListEditor defaultValue={["a", "b"]} onValueChange={onValueChange} />);
28
+ const input = screen.getByRole("textbox", { name: "Item 1" });
29
+ await userEvent.type(input, "x");
30
+ expect(onValueChange).toHaveBeenLastCalledWith(["ax", "b"]);
31
+ });
32
+
33
+ it("removes a row via the remove button", async () => {
34
+ const onValueChange = vi.fn();
35
+ render(<ListEditor defaultValue={["a", "b"]} onValueChange={onValueChange} />);
36
+ await userEvent.click(screen.getByRole("button", { name: "Remove item 1" }));
37
+ expect(onValueChange).toHaveBeenCalledWith(["b"]);
38
+ });
39
+
40
+ it("moves a row up via the move-up button (keyboard-operable, real <button>)", async () => {
41
+ const onValueChange = vi.fn();
42
+ render(<ListEditor defaultValue={["a", "b", "c"]} onValueChange={onValueChange} />);
43
+ const moveUp = screen.getByRole("button", { name: "Move item 2 up" });
44
+ expect(moveUp.tagName).toBe("BUTTON");
45
+ await userEvent.click(moveUp);
46
+ expect(onValueChange).toHaveBeenCalledWith(["b", "a", "c"]);
47
+ });
48
+
49
+ it("moves a row down via the move-down button", async () => {
50
+ const onValueChange = vi.fn();
51
+ render(<ListEditor defaultValue={["a", "b", "c"]} onValueChange={onValueChange} />);
52
+ await userEvent.click(screen.getByRole("button", { name: "Move item 1 down" }));
53
+ expect(onValueChange).toHaveBeenCalledWith(["b", "a", "c"]);
54
+ });
55
+
56
+ it("disables move-up on the first row and move-down on the last row", () => {
57
+ render(<ListEditor defaultValue={["a", "b"]} />);
58
+ expect(screen.getByRole("button", { name: "Move item 1 up" })).toBeDisabled();
59
+ expect(screen.getByRole("button", { name: "Move item 2 down" })).toBeDisabled();
60
+ });
61
+
62
+ it("move-up/move-down buttons are reachable via Tab and activate via Enter", async () => {
63
+ const onValueChange = vi.fn();
64
+ render(<ListEditor defaultValue={["a", "b"]} onValueChange={onValueChange} />);
65
+ const moveDown = screen.getByRole("button", { name: "Move item 1 down" });
66
+ moveDown.focus();
67
+ expect(moveDown).toHaveFocus();
68
+ await userEvent.keyboard("{Enter}");
69
+ expect(onValueChange).toHaveBeenCalledWith(["b", "a"]);
70
+ });
71
+
72
+ it("hides reorder buttons when reorderable=false", () => {
73
+ render(<ListEditor defaultValue={["a", "b"]} reorderable={false} />);
74
+ // Anchored at the start — "Remove item 1" contains the substring "move
75
+ // item" (as in "Re-move item"), so an unanchored /move item/i overmatches.
76
+ expect(screen.queryByRole("button", { name: /^move item/i })).not.toBeInTheDocument();
77
+ });
78
+
79
+ it("enforces max row count", () => {
80
+ render(<ListEditor defaultValue={["a", "b"]} max={2} />);
81
+ expect(screen.getByRole("button", { name: /add item/i })).toBeDisabled();
82
+ });
83
+
84
+ it("supports the controlled mode via value/onValueChange", async () => {
85
+ const onValueChange = vi.fn();
86
+ render(<ListEditor value={["a"]} onValueChange={onValueChange} />);
87
+ await userEvent.click(screen.getByRole("button", { name: /add item/i }));
88
+ expect(onValueChange).toHaveBeenCalledWith(["a", ""]);
89
+ });
90
+
91
+ it("is disabled when disabled prop is set", () => {
92
+ render(<ListEditor defaultValue={["a"]} disabled />);
93
+ expect(screen.getByRole("textbox", { name: "Item 1" })).toBeDisabled();
94
+ expect(screen.getByRole("button", { name: /add item/i })).toBeDisabled();
95
+ });
96
+ });
@@ -0,0 +1,192 @@
1
+ "use client";
2
+
3
+ import {
4
+ forwardRef,
5
+ useCallback,
6
+ useMemo,
7
+ useState,
8
+ type HTMLAttributes,
9
+ type ReactNode,
10
+ } from "react";
11
+ import { ChevronDown, ChevronUp, Plus, X } from "lucide-react";
12
+ import { cn } from "../../lib/cn";
13
+ import { Button } from "../button";
14
+ import { Input } from "../input";
15
+ import { useLocale } from "../locale-provider";
16
+
17
+ export interface ListEditorProps extends Omit<
18
+ HTMLAttributes<HTMLDivElement>,
19
+ "onChange" | "defaultValue"
20
+ > {
21
+ /** Controlled list of strings. */
22
+ value?: string[];
23
+ /** Uncontrolled initial list of strings. */
24
+ defaultValue?: string[];
25
+ /** Called when the list changes (add, remove, edit, or reorder). */
26
+ onValueChange?: (value: string[]) => void;
27
+ /**
28
+ * Whether rows can be reordered via move-up/move-down buttons. Default
29
+ * true. Reordering is keyboard-operable (real buttons), never drag-only.
30
+ */
31
+ reorderable?: boolean;
32
+ /** Placeholder shown in each row's text field. */
33
+ placeholder?: string;
34
+ /** Maximum number of rows allowed. */
35
+ max?: number;
36
+ /** Whether the editor is disabled. */
37
+ disabled?: boolean;
38
+ /** Label for the "add item" control. */
39
+ addLabel?: ReactNode;
40
+ }
41
+
42
+ /**
43
+ * ListEditor — one `Input` per row over a `string[]`, with add / remove /
44
+ * keyboard-operable reorder (move-up/move-down buttons, not drag-and-drop —
45
+ * no drag library exists in the monorepo and none should be added for this).
46
+ *
47
+ * Controlled via `value`/`onValueChange`; uncontrolled via `defaultValue`.
48
+ */
49
+ export const ListEditor = forwardRef<HTMLDivElement, ListEditorProps>(function ListEditor(
50
+ {
51
+ value: valueProp,
52
+ defaultValue,
53
+ onValueChange,
54
+ reorderable = true,
55
+ placeholder,
56
+ max,
57
+ disabled,
58
+ addLabel,
59
+ className,
60
+ ...props
61
+ },
62
+ ref,
63
+ ) {
64
+ const { t } = useLocale();
65
+ const isControlled = valueProp !== undefined;
66
+ const [internalValue, setInternalValue] = useState<string[]>(defaultValue ?? []);
67
+ const rows = useMemo(
68
+ () => (isControlled ? (valueProp ?? []) : internalValue),
69
+ [isControlled, valueProp, internalValue],
70
+ );
71
+
72
+ const commit = useCallback(
73
+ (next: string[]) => {
74
+ if (!isControlled) setInternalValue(next);
75
+ onValueChange?.(next);
76
+ },
77
+ [isControlled, onValueChange],
78
+ );
79
+
80
+ const addRow = useCallback(() => {
81
+ if (max !== undefined && rows.length >= max) return;
82
+ commit([...rows, ""]);
83
+ }, [rows, max, commit]);
84
+
85
+ const updateRow = useCallback(
86
+ (index: number, next: string) => {
87
+ commit(rows.map((row, i) => (i === index ? next : row)));
88
+ },
89
+ [rows, commit],
90
+ );
91
+
92
+ const removeRow = useCallback(
93
+ (index: number) => {
94
+ commit(rows.filter((_, i) => i !== index));
95
+ },
96
+ [rows, commit],
97
+ );
98
+
99
+ const moveRow = useCallback(
100
+ (index: number, direction: -1 | 1) => {
101
+ const target = index + direction;
102
+ if (target < 0 || target >= rows.length) return;
103
+ const next = [...rows];
104
+ const [moved] = next.splice(index, 1);
105
+ next.splice(target, 0, moved!);
106
+ commit(next);
107
+ },
108
+ [rows, commit],
109
+ );
110
+
111
+ const atMax = max !== undefined && rows.length >= max;
112
+
113
+ return (
114
+ <div
115
+ ref={ref}
116
+ data-slot="list-editor"
117
+ className={cn("flex flex-col gap-2", className)}
118
+ {...props}
119
+ >
120
+ {rows.length === 0 ? (
121
+ <p data-slot="list-editor-empty" className="text-body text-muted-foreground">
122
+ {t("ui.listEditor.empty")}
123
+ </p>
124
+ ) : (
125
+ <div className="flex flex-col gap-2">
126
+ {rows.map((row, index) => (
127
+ <div key={index} data-slot="list-editor-row" className="flex items-center gap-1">
128
+ <Input
129
+ data-slot="list-editor-item"
130
+ value={row}
131
+ placeholder={placeholder}
132
+ disabled={disabled}
133
+ aria-label={t("ui.listEditor.itemLabel", { n: index + 1 })}
134
+ onChange={(event) => updateRow(index, event.target.value)}
135
+ className="flex-1"
136
+ />
137
+ {reorderable && (
138
+ <>
139
+ <Button
140
+ data-slot="list-editor-move-up"
141
+ type="button"
142
+ variant="ghost"
143
+ size="icon-sm"
144
+ disabled={disabled || index === 0}
145
+ aria-label={t("ui.listEditor.moveUp", { n: index + 1 })}
146
+ onClick={() => moveRow(index, -1)}
147
+ >
148
+ <ChevronUp aria-hidden="true" />
149
+ </Button>
150
+ <Button
151
+ data-slot="list-editor-move-down"
152
+ type="button"
153
+ variant="ghost"
154
+ size="icon-sm"
155
+ disabled={disabled || index === rows.length - 1}
156
+ aria-label={t("ui.listEditor.moveDown", { n: index + 1 })}
157
+ onClick={() => moveRow(index, 1)}
158
+ >
159
+ <ChevronDown aria-hidden="true" />
160
+ </Button>
161
+ </>
162
+ )}
163
+ <Button
164
+ data-slot="list-editor-remove"
165
+ type="button"
166
+ variant="ghost"
167
+ size="icon-sm"
168
+ disabled={disabled}
169
+ aria-label={t("ui.listEditor.removeItem", { n: index + 1 })}
170
+ onClick={() => removeRow(index)}
171
+ >
172
+ <X aria-hidden="true" />
173
+ </Button>
174
+ </div>
175
+ ))}
176
+ </div>
177
+ )}
178
+ <Button
179
+ data-slot="list-editor-add"
180
+ type="button"
181
+ variant="outline-subtle"
182
+ size="sm"
183
+ disabled={disabled || atMax}
184
+ onClick={addRow}
185
+ className="self-start"
186
+ >
187
+ <Plus aria-hidden="true" />
188
+ {addLabel ?? t("ui.listEditor.addItem")}
189
+ </Button>
190
+ </div>
191
+ );
192
+ });
@@ -0,0 +1 @@
1
+ export { LoadingState, type LoadingStateProps } from "./loading-state";
@@ -0,0 +1,38 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { LoadingState } from "./loading-state";
3
+
4
+ const meta = {
5
+ title: "States/LoadingState",
6
+ component: LoadingState,
7
+ tags: ["autodocs"],
8
+ parameters: {
9
+ docs: {
10
+ description: {
11
+ component:
12
+ '@deprecated — use `<StatePanel kind="loading" />` instead. Thin back-compat wrapper.',
13
+ },
14
+ },
15
+ },
16
+ argTypes: {
17
+ label: {
18
+ description: 'Accessible status label shown below the spinner. Default "Loading…".',
19
+ control: "text",
20
+ table: { category: "Content" },
21
+ },
22
+ size: {
23
+ description: "Visual size of the spinner.",
24
+ control: { type: "radio" },
25
+ options: ["sm", "md", "lg"],
26
+ table: { category: "Appearance" },
27
+ },
28
+ className: {
29
+ description: "Additional CSS classes applied to the container.",
30
+ control: "text",
31
+ table: { category: "Styling" },
32
+ },
33
+ },
34
+ } satisfies Meta<typeof LoadingState>;
35
+ export default meta;
36
+ type Story = StoryObj<typeof meta>;
37
+
38
+ export const Default: Story = { args: { label: "Loading workspace…" } };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @deprecated Use `StatePanel kind="loading"` from `@elabs-ai/components-ui` instead.
3
+ * This wrapper is kept for back-compat and will be removed in a future release.
4
+ */
5
+ import { type ReactNode } from "react";
6
+ import { StatePanel } from "../state-panel/state-panel";
7
+
8
+ export interface LoadingStateProps {
9
+ /** Accessible status label. Defaults to "Loading…". */
10
+ label?: ReactNode;
11
+ /** Visual size of the spinner. */
12
+ size?: "sm" | "md" | "lg";
13
+ className?: string;
14
+ }
15
+
16
+ /** @deprecated Use `<StatePanel kind="loading" />` instead. */
17
+ export function LoadingState({ label = "Loading…", size = "md", className }: LoadingStateProps) {
18
+ return (
19
+ <StatePanel
20
+ kind="loading"
21
+ loadingLabel={typeof label === "string" ? label : undefined}
22
+ size={size}
23
+ className={className}
24
+ />
25
+ );
26
+ }
@@ -0,0 +1,8 @@
1
+ export {
2
+ LocaleProvider,
3
+ useLocale,
4
+ type LocaleContextValue,
5
+ type LocaleProviderProps,
6
+ type Messages,
7
+ } from "./locale-provider";
8
+ export { DEFAULT_MESSAGES } from "./messages";
@@ -0,0 +1,83 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { LocaleProvider, useLocale } from "./locale-provider";
3
+
4
+ const meta = {
5
+ title: "Providers/LocaleProvider",
6
+ component: LocaleProvider,
7
+ tags: ["autodocs"],
8
+ } satisfies Meta<typeof LocaleProvider>;
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Demo component: reads the active locale context
15
+ // ---------------------------------------------------------------------------
16
+
17
+ function LocaleDemo() {
18
+ const { locale, dir, t, formatNumber, formatDate } = useLocale();
19
+ return (
20
+ <div className="space-y-2 rounded-md border border-border p-4 text-body">
21
+ <p>
22
+ <span className="font-medium text-muted-foreground">locale: </span>
23
+ {locale}
24
+ </p>
25
+ <p>
26
+ <span className="font-medium text-muted-foreground">dir: </span>
27
+ {dir}
28
+ </p>
29
+ <p>
30
+ <span className="font-medium text-muted-foreground">t(&quot;next&quot;): </span>
31
+ {t("next")}
32
+ </p>
33
+ <p>
34
+ <span className="font-medium text-muted-foreground">t(&quot;previous&quot;): </span>
35
+ {t("previous")}
36
+ </p>
37
+ <p>
38
+ <span className="font-medium text-muted-foreground">t(&quot;close&quot;): </span>
39
+ {t("close")}
40
+ </p>
41
+ <p>
42
+ <span className="font-medium text-muted-foreground">
43
+ formatNumber(1234567.89, currency EUR):{" "}
44
+ </span>
45
+ {formatNumber(1234567.89, { style: "currency", currency: "EUR" })}
46
+ </p>
47
+ <p>
48
+ <span className="font-medium text-muted-foreground">formatDate(2026-06-08): </span>
49
+ {formatDate(new Date(2026, 5, 8))}
50
+ </p>
51
+ </div>
52
+ );
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Stories
57
+ // ---------------------------------------------------------------------------
58
+
59
+ /** English (en-US) locale — the default. */
60
+ export const Default: Story = {
61
+ render: () => (
62
+ <LocaleProvider locale="en-US" dir="ltr">
63
+ <LocaleDemo />
64
+ </LocaleProvider>
65
+ ),
66
+ };
67
+
68
+ /**
69
+ * Arabic (ar-EG) locale with RTL direction.
70
+ * Overrides `next` and `previous` keys; other keys fall back to the English defaults.
71
+ */
72
+ export const RtlArabic: Story = {
73
+ render: () => (
74
+ <LocaleProvider locale="ar-EG" dir="rtl" messages={{ next: "التالي", previous: "السابق" }}>
75
+ <LocaleDemo />
76
+ </LocaleProvider>
77
+ ),
78
+ };
79
+
80
+ /** Without a provider — `useLocale` degrades to en-US / ltr defaults. */
81
+ export const NoProvider: Story = {
82
+ render: () => <LocaleDemo />,
83
+ };
@@ -0,0 +1,238 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { createPortal } from "react-dom";
4
+ import { useDirection } from "@radix-ui/react-direction";
5
+ import { LocaleProvider, useLocale } from "./locale-provider";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Helper component — renders all locale values as text
9
+ // ---------------------------------------------------------------------------
10
+
11
+ function LocaleReader({ numberInput, dateInput }: { numberInput?: number; dateInput?: Date }) {
12
+ const { locale, dir, t, formatNumber, formatDate } = useLocale();
13
+ return (
14
+ <div>
15
+ <span data-testid="locale">{locale}</span>
16
+ <span data-testid="dir">{dir}</span>
17
+ <span data-testid="close">{t("close")}</span>
18
+ <span data-testid="next">{t("next")}</span>
19
+ <span data-testid="previous">{t("previous")}</span>
20
+ {numberInput !== undefined && <span data-testid="number">{formatNumber(numberInput)}</span>}
21
+ {dateInput !== undefined && <span data-testid="date">{formatDate(dateInput)}</span>}
22
+ </div>
23
+ );
24
+ }
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Tests
28
+ // ---------------------------------------------------------------------------
29
+
30
+ describe("useLocale (no provider)", () => {
31
+ it("returns en-US locale and ltr direction", () => {
32
+ render(<LocaleReader />);
33
+ expect(screen.getByTestId("locale").textContent).toBe("en-US");
34
+ expect(screen.getByTestId("dir").textContent).toBe("ltr");
35
+ });
36
+
37
+ it("t('close') returns the English default 'Close'", () => {
38
+ render(<LocaleReader />);
39
+ expect(screen.getByTestId("close").textContent).toBe("Close");
40
+ });
41
+
42
+ it("t('next') returns the English default 'Next'", () => {
43
+ render(<LocaleReader />);
44
+ expect(screen.getByTestId("next").textContent).toBe("Next");
45
+ });
46
+ });
47
+
48
+ describe("LocaleProvider — default en-US", () => {
49
+ it("provides locale and dir to children", () => {
50
+ render(
51
+ <LocaleProvider locale="en-US" dir="ltr">
52
+ <LocaleReader />
53
+ </LocaleProvider>,
54
+ );
55
+ expect(screen.getByTestId("locale").textContent).toBe("en-US");
56
+ expect(screen.getByTestId("dir").textContent).toBe("ltr");
57
+ });
58
+
59
+ it("applies dir attribute to the wrapper element", () => {
60
+ const { container } = render(
61
+ <LocaleProvider locale="en-US" dir="ltr">
62
+ <span>content</span>
63
+ </LocaleProvider>,
64
+ );
65
+ const wrapper = container.firstElementChild as HTMLElement;
66
+ expect(wrapper.getAttribute("dir")).toBe("ltr");
67
+ });
68
+
69
+ it("t('close') returns English default when no messages override provided", () => {
70
+ render(
71
+ <LocaleProvider>
72
+ <LocaleReader />
73
+ </LocaleProvider>,
74
+ );
75
+ expect(screen.getByTestId("close").textContent).toBe("Close");
76
+ });
77
+ });
78
+
79
+ describe("LocaleProvider — custom messages", () => {
80
+ it("overrides t() for the provided keys", () => {
81
+ render(
82
+ <LocaleProvider messages={{ next: "Weiter", previous: "Zurück" }}>
83
+ <LocaleReader />
84
+ </LocaleProvider>,
85
+ );
86
+ expect(screen.getByTestId("next").textContent).toBe("Weiter");
87
+ expect(screen.getByTestId("previous").textContent).toBe("Zurück");
88
+ });
89
+
90
+ it("non-overridden keys fall through to English defaults", () => {
91
+ render(
92
+ <LocaleProvider messages={{ next: "Weiter" }}>
93
+ <LocaleReader />
94
+ </LocaleProvider>,
95
+ );
96
+ // "close" was not overridden
97
+ expect(screen.getByTestId("close").textContent).toBe("Close");
98
+ });
99
+ });
100
+
101
+ describe("LocaleProvider — RTL Arabic", () => {
102
+ it("provides ar-EG locale and rtl direction", () => {
103
+ render(
104
+ <LocaleProvider locale="ar-EG" dir="rtl" messages={{ next: "التالي", previous: "السابق" }}>
105
+ <LocaleReader />
106
+ </LocaleProvider>,
107
+ );
108
+ expect(screen.getByTestId("locale").textContent).toBe("ar-EG");
109
+ expect(screen.getByTestId("dir").textContent).toBe("rtl");
110
+ });
111
+
112
+ it("applies dir='rtl' on the wrapper element", () => {
113
+ const { container } = render(
114
+ <LocaleProvider locale="ar-EG" dir="rtl">
115
+ <span>content</span>
116
+ </LocaleProvider>,
117
+ );
118
+ const wrapper = container.firstElementChild as HTMLElement;
119
+ expect(wrapper.getAttribute("dir")).toBe("rtl");
120
+ });
121
+
122
+ it("t() returns overridden Arabic strings", () => {
123
+ render(
124
+ <LocaleProvider locale="ar-EG" dir="rtl" messages={{ next: "التالي", previous: "السابق" }}>
125
+ <LocaleReader />
126
+ </LocaleProvider>,
127
+ );
128
+ expect(screen.getByTestId("next").textContent).toBe("التالي");
129
+ expect(screen.getByTestId("previous").textContent).toBe("السابق");
130
+ });
131
+ });
132
+
133
+ describe("LocaleProvider — formatNumber", () => {
134
+ it("formats a number in en-US locale", () => {
135
+ render(
136
+ <LocaleProvider locale="en-US">
137
+ <LocaleReader numberInput={1234567.89} />
138
+ </LocaleProvider>,
139
+ );
140
+ // en-US formats with comma thousands separator
141
+ const text = screen.getByTestId("number").textContent ?? "";
142
+ expect(text).toMatch(/1[,.]?234/);
143
+ });
144
+
145
+ it("formats without a provider (en-US default)", () => {
146
+ render(<LocaleReader numberInput={42} />);
147
+ expect(screen.getByTestId("number").textContent).toBe("42");
148
+ });
149
+ });
150
+
151
+ describe("LocaleProvider — formatDate", () => {
152
+ it("formats a date in en-US locale", () => {
153
+ render(
154
+ <LocaleProvider locale="en-US">
155
+ <LocaleReader dateInput={new Date(2026, 5, 8)} />
156
+ </LocaleProvider>,
157
+ );
158
+ const text = screen.getByTestId("date").textContent ?? "";
159
+ // en-US default format includes the year 2026
160
+ expect(text).toContain("2026");
161
+ });
162
+
163
+ it("formats without a provider (en-US default)", () => {
164
+ render(<LocaleReader dateInput={new Date(2026, 5, 8)} />);
165
+ const text = screen.getByTestId("date").textContent ?? "";
166
+ expect(text).toContain("2026");
167
+ });
168
+ });
169
+
170
+ // ---------------------------------------------------------------------------
171
+ // DirectionProvider — RTL reaches portalled content (ADR-0014, #181)
172
+ // ---------------------------------------------------------------------------
173
+
174
+ /**
175
+ * Reads Radix's direction context (the channel every portalled Radix overlay
176
+ * uses) and renders it through a PORTAL to document.body — i.e. outside the
177
+ * `<div dir>` DOM ancestor. React context flows through portals, so this proves
178
+ * `DirectionProvider` (not the DOM `dir`) is what carries direction to overlays.
179
+ */
180
+ function PortaledDirectionReader() {
181
+ const dir = useDirection();
182
+ return createPortal(<span data-testid="portal-dir">{dir}</span>, document.body);
183
+ }
184
+
185
+ describe("LocaleProvider — DirectionProvider (RTL portals)", () => {
186
+ it("provides dir='rtl' to portalled content via Radix direction context", () => {
187
+ render(
188
+ <LocaleProvider locale="ar-EG" dir="rtl">
189
+ <PortaledDirectionReader />
190
+ </LocaleProvider>,
191
+ );
192
+ expect(screen.getByTestId("portal-dir").textContent).toBe("rtl");
193
+ });
194
+
195
+ it("defaults portalled content to ltr", () => {
196
+ render(
197
+ <LocaleProvider>
198
+ <PortaledDirectionReader />
199
+ </LocaleProvider>,
200
+ );
201
+ expect(screen.getByTestId("portal-dir").textContent).toBe("ltr");
202
+ });
203
+ });
204
+
205
+ describe("t() — variable interpolation", () => {
206
+ function Interpolated({ vars }: { vars: Record<string, string | number> }) {
207
+ const { t } = useLocale();
208
+ return <span data-testid="result">{t("greeting", vars)}</span>;
209
+ }
210
+
211
+ it("substitutes {name}-style placeholders", () => {
212
+ render(
213
+ <LocaleProvider messages={{ greeting: "Hello, {name}!" }}>
214
+ <Interpolated vars={{ name: "World" }} />
215
+ </LocaleProvider>,
216
+ );
217
+ expect(screen.getByTestId("result").textContent).toBe("Hello, World!");
218
+ });
219
+
220
+ it("leaves unknown placeholders intact", () => {
221
+ render(
222
+ <LocaleProvider messages={{ greeting: "Hi {user}" }}>
223
+ <Interpolated vars={{ notUser: "Alice" }} />
224
+ </LocaleProvider>,
225
+ );
226
+ expect(screen.getByTestId("result").textContent).toBe("Hi {user}");
227
+ });
228
+
229
+ it("returns the key itself when the key is absent", () => {
230
+ render(
231
+ <LocaleProvider>
232
+ <Interpolated vars={{}} />
233
+ </LocaleProvider>,
234
+ );
235
+ // "greeting" is not in DEFAULT_MESSAGES
236
+ expect(screen.getByTestId("result").textContent).toBe("greeting");
237
+ });
238
+ });