@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,100 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { ChevronsUpDown, Plus } from "lucide-react";
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuItem,
9
+ DropdownMenuLabel,
10
+ DropdownMenuSeparator,
11
+ DropdownMenuShortcut,
12
+ DropdownMenuTrigger,
13
+ } from "../dropdown-menu";
14
+ import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from "../sidebar";
15
+ import { cn } from "../../lib/cn";
16
+
17
+ export type TeamSwitcherTeam = {
18
+ name: string;
19
+ logo: React.ElementType;
20
+ plan: string;
21
+ };
22
+
23
+ export interface TeamSwitcherProps {
24
+ teams: TeamSwitcherTeam[];
25
+ /**
26
+ * Tailwind class for the active-team logo container background.
27
+ * @default "bg-sidebar-accent text-sidebar-accent-foreground"
28
+ */
29
+ logoClassName?: string;
30
+ className?: string;
31
+ }
32
+
33
+ /**
34
+ * Sidebar team/workspace switcher. Shared primitive reconciled from sidebar-02
35
+ * and sidebar-05 blocks (issue #99). Copy-owned blocks should import this
36
+ * instead of maintaining local duplicates.
37
+ */
38
+ export function TeamSwitcher({ teams, logoClassName, className }: TeamSwitcherProps) {
39
+ const { isMobile } = useSidebar();
40
+ const [activeTeam, setActiveTeam] = React.useState(teams[0]);
41
+ if (!activeTeam) return null;
42
+ const Logo = activeTeam.logo;
43
+
44
+ return (
45
+ <SidebarMenu className={className}>
46
+ <SidebarMenuItem>
47
+ <DropdownMenu>
48
+ <DropdownMenuTrigger asChild>
49
+ <SidebarMenuButton
50
+ size="lg"
51
+ className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
52
+ >
53
+ <div
54
+ className={cn(
55
+ "flex aspect-square size-8 items-center justify-center rounded-lg",
56
+ logoClassName ?? "bg-sidebar-accent text-sidebar-accent-foreground",
57
+ )}
58
+ >
59
+ <Logo className="size-4" />
60
+ </div>
61
+ <div className="grid flex-1 text-start text-sm leading-tight">
62
+ <span className="truncate font-semibold">{activeTeam.name}</span>
63
+ <span className="truncate text-xs">{activeTeam.plan}</span>
64
+ </div>
65
+ <ChevronsUpDown className="ms-auto" />
66
+ </SidebarMenuButton>
67
+ </DropdownMenuTrigger>
68
+ <DropdownMenuContent
69
+ className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
70
+ align="start"
71
+ side={isMobile ? "bottom" : "right"}
72
+ sideOffset={4}
73
+ >
74
+ <DropdownMenuLabel className="text-xs text-muted-foreground">Teams</DropdownMenuLabel>
75
+ {teams.map((team, index) => (
76
+ <DropdownMenuItem
77
+ key={team.name}
78
+ onClick={() => setActiveTeam(team)}
79
+ className="gap-2 p-2"
80
+ >
81
+ <div className="flex size-6 items-center justify-center rounded-md border">
82
+ <team.logo className="size-4 shrink-0" />
83
+ </div>
84
+ {team.name}
85
+ <DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
86
+ </DropdownMenuItem>
87
+ ))}
88
+ <DropdownMenuSeparator />
89
+ <DropdownMenuItem className="gap-2 p-2">
90
+ <div className="flex size-6 items-center justify-center rounded-md border bg-transparent">
91
+ <Plus className="size-4" />
92
+ </div>
93
+ <div className="font-medium text-muted-foreground">Add team</div>
94
+ </DropdownMenuItem>
95
+ </DropdownMenuContent>
96
+ </DropdownMenu>
97
+ </SidebarMenuItem>
98
+ </SidebarMenu>
99
+ );
100
+ }
@@ -0,0 +1 @@
1
+ export { Textarea, type TextareaProps } from "./textarea";
@@ -0,0 +1,42 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Textarea } from "./textarea";
3
+
4
+ const meta = {
5
+ title: "Core/Textarea",
6
+ component: Textarea,
7
+ tags: ["autodocs"],
8
+ args: { placeholder: "Write a message…", rows: 4 },
9
+ argTypes: {
10
+ placeholder: {
11
+ description: "Placeholder hint shown when the field is empty.",
12
+ control: "text",
13
+ table: { category: "Content" },
14
+ },
15
+ rows: {
16
+ description: "Initial visible row count (the element can still be resized by the user).",
17
+ control: "number",
18
+ table: { category: "Appearance" },
19
+ },
20
+ disabled: {
21
+ description: "Disables the textarea — cursor-not-allowed, reduced opacity.",
22
+ control: "boolean",
23
+ table: { category: "State" },
24
+ },
25
+ "aria-invalid": {
26
+ description: "Marks the field invalid — shows destructive ring / border.",
27
+ control: "boolean",
28
+ table: { category: "State" },
29
+ },
30
+ className: {
31
+ description: "Extra Tailwind classes merged via cn().",
32
+ control: "text",
33
+ table: { category: "Appearance" },
34
+ },
35
+ },
36
+ } satisfies Meta<typeof Textarea>;
37
+ export default meta;
38
+ type Story = StoryObj<typeof meta>;
39
+
40
+ export const Default: Story = {};
41
+ export const Disabled: Story = { args: { disabled: true, value: "Disabled content" } };
42
+ export const Invalid: Story = { args: { "aria-invalid": true, defaultValue: "Invalid content" } };
@@ -0,0 +1,24 @@
1
+ import { forwardRef, type TextareaHTMLAttributes } from "react";
2
+ import { cn } from "../../lib/cn";
3
+
4
+ export type TextareaProps = TextareaHTMLAttributes<HTMLTextAreaElement>;
5
+
6
+ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea(
7
+ { className, ...props },
8
+ ref,
9
+ ) {
10
+ return (
11
+ <textarea
12
+ ref={ref}
13
+ className={cn(
14
+ "flex min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-[color,background-color,border-color,box-shadow] duration-fast ease-standard",
15
+ "placeholder:text-muted-foreground",
16
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
17
+ "disabled:cursor-not-allowed disabled:opacity-50 disabled:border-border",
18
+ "aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive",
19
+ className,
20
+ )}
21
+ {...props}
22
+ />
23
+ );
24
+ });
@@ -0,0 +1,2 @@
1
+ export { ThemeSwitcher, type ThemeSwitcherProps, type ThemePreference } from "./theme-switcher";
2
+ export { useThemeTransition, type ThemeTransitionEffect } from "./use-theme-transition";
@@ -0,0 +1,146 @@
1
+ import { useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react-vite";
3
+ import { expect, waitFor, within } from "storybook/test";
4
+ import {
5
+ BUILT_IN_THEMES,
6
+ BUILT_IN_THEME_DEFINITIONS,
7
+ defineTheme,
8
+ ThemeProvider,
9
+ } from "@elabs-ai/components-tokens";
10
+
11
+ import { ThemeSwitcher, type ThemePreference } from "./theme-switcher";
12
+
13
+ const meta = {
14
+ title: "Core/ThemeSwitcher",
15
+ component: ThemeSwitcher,
16
+ tags: ["autodocs"],
17
+ decorators: [
18
+ (Story) => (
19
+ <ThemeProvider>
20
+ <div className="flex min-h-32 items-center justify-center">
21
+ <Story />
22
+ </div>
23
+ </ThemeProvider>
24
+ ),
25
+ ],
26
+ argTypes: {
27
+ mode: {
28
+ description: "Force presentation mode; `auto` picks toggle vs dropdown by theme count.",
29
+ control: "inline-radio",
30
+ options: ["auto", "toggle", "dropdown"],
31
+ table: { category: "Behavior" },
32
+ },
33
+ effect: {
34
+ description: "Whole-screen reveal animation used when switching themes.",
35
+ control: { type: "select" },
36
+ options: ["polygon", "circle", "circle-blur", "triangle"],
37
+ table: { category: "Appearance" },
38
+ },
39
+ size: {
40
+ description: "Button size of the trigger.",
41
+ control: "inline-radio",
42
+ options: ["sm", "default", "lg"],
43
+ table: { category: "Appearance" },
44
+ },
45
+ showSystem: {
46
+ description: 'Whether to offer a "System" option that follows the OS color scheme.',
47
+ control: "boolean",
48
+ table: { category: "Behavior" },
49
+ },
50
+ themes: {
51
+ description: "Theme names to offer. ≤2 → toggle; >2 → dropdown.",
52
+ control: false,
53
+ table: { category: "Behavior" },
54
+ },
55
+ },
56
+ } satisfies Meta<typeof ThemeSwitcher>;
57
+ export default meta;
58
+
59
+ type Story = StoryObj<typeof meta>;
60
+
61
+ /** Default: the built-in light/dark pair → a cycling light / dark / system toggle. */
62
+ export const Toggle: Story = {
63
+ play: async ({ canvas }) => {
64
+ // The toggle button is present and has an accessible label describing the cycle action.
65
+ const btn = canvas.getByRole("button", { name: /theme/i });
66
+ await expect(btn).toBeInTheDocument();
67
+ await expect(btn).toHaveAttribute("aria-label");
68
+ },
69
+ };
70
+
71
+ /** Dropdown mode — the list form. (More than two themes picks this automatically.) */
72
+ export const Dropdown: Story = { args: { mode: "dropdown", themes: [...BUILT_IN_THEMES] } };
73
+
74
+ /** Toggle without the System option (light / dark only). */
75
+ export const ToggleNoSystem: Story = { args: { showSystem: false } };
76
+
77
+ /** A custom (non-default) theme pair. ≤2 themes still toggle instead of a dropdown. */
78
+ export const CustomPair: Story = { args: { themes: ["dark", "light"] } };
79
+
80
+ /**
81
+ * Controlled mode (#366): the caller owns the preference — including
82
+ * "system" — via `preference`/`onPreferenceChange` instead of the switcher's
83
+ * own `localStorage`-backed state, e.g. a settings panel synced to a server.
84
+ * The label below reflects the plain `useState` living OUTSIDE `ThemeSwitcher`.
85
+ */
86
+ export const Controlled: Story = {
87
+ render: function ControlledStory(args) {
88
+ const [preference, setPreference] = useState<ThemePreference>("system");
89
+ return (
90
+ <div className="flex flex-col items-center gap-2">
91
+ <ThemeSwitcher {...args} preference={preference} onPreferenceChange={setPreference} />
92
+ <p className="text-caption text-muted-foreground">Preference: {preference}</p>
93
+ </div>
94
+ );
95
+ },
96
+ args: { themes: ["light", "dark"] },
97
+ play: async ({ canvas, userEvent }) => {
98
+ await expect(canvas.getByText("Preference: system")).toBeInTheDocument();
99
+ const trigger = canvas.getByRole("button", { name: /theme: system/i });
100
+ await userEvent.click(trigger);
101
+ // "system" cycles to "light" (light) — the caller's own state updates,
102
+ // and the switcher renders straight from it (never its own storage key).
103
+ await expect(canvas.getByText("Preference: light")).toBeInTheDocument();
104
+ },
105
+ };
106
+
107
+ /**
108
+ * When a `ThemeProvider` restricts `allowedThemes` (#355), the switcher
109
+ * automatically narrows to that subset (#384). The provider below registers a
110
+ * THIRD theme on top of the two reference ones and then allows only Light and
111
+ * Dark — so the narrowing is observable: `Drafting` is unreachable via any menu
112
+ * item, "System", or the OS `prefers-color-scheme` listener. The nested
113
+ * `ThemeProvider` overrides this file's meta-level (unrestricted) one, since
114
+ * `useTheme()` always reads the nearest ancestor.
115
+ */
116
+ const draftingTheme = defineTheme({ value: "drafting", label: "Drafting", dark: false });
117
+ const RESTRICTED_REGISTRY = [...BUILT_IN_THEME_DEFINITIONS, draftingTheme];
118
+
119
+ export const RestrictedProvider: Story = {
120
+ decorators: [
121
+ (Story) => (
122
+ <ThemeProvider
123
+ themes={RESTRICTED_REGISTRY}
124
+ allowedThemes={["light", "dark"]}
125
+ storageKey={null}
126
+ >
127
+ <div className="flex min-h-32 items-center justify-center">
128
+ <Story />
129
+ </div>
130
+ </ThemeProvider>
131
+ ),
132
+ ],
133
+ args: { mode: "dropdown", themes: RESTRICTED_REGISTRY.map((t) => t.value) },
134
+ play: async ({ canvas, canvasElement, userEvent }) => {
135
+ const trigger = canvas.getByRole("button", { name: "Theme" });
136
+ await userEvent.click(trigger);
137
+ const body = within(canvasElement.ownerDocument.body);
138
+ const dark = await body.findByRole("menuitem", { name: /dark/i });
139
+ await waitFor(() => expect(dark).toBeVisible());
140
+ await expect(body.queryByText(/drafting/i)).not.toBeInTheDocument();
141
+ // Close the menu so the story doesn't end mid-interaction (portaled content
142
+ // still open at test-end confuses the a11y pass on the surrounding page).
143
+ await userEvent.keyboard("{Escape}");
144
+ await waitFor(() => expect(body.queryByRole("menu")).not.toBeInTheDocument());
145
+ },
146
+ };
@@ -0,0 +1,253 @@
1
+ import type { ReactElement } from "react";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { render, screen, within } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+ import {
6
+ BUILT_IN_THEMES,
7
+ BUILT_IN_THEME_DEFINITIONS,
8
+ defineTheme,
9
+ ThemeProvider,
10
+ } from "@elabs-ai/components-tokens";
11
+
12
+ import { ThemeSwitcher } from "./theme-switcher";
13
+
14
+ const SYSTEM_STORAGE_KEY = "brand-ui-theme-system";
15
+
16
+ function setup(ui: ReactElement) {
17
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
18
+ }
19
+
20
+ beforeEach(() => {
21
+ window.localStorage.clear();
22
+ document.documentElement.removeAttribute("data-theme");
23
+ });
24
+
25
+ describe("ThemeSwitcher", () => {
26
+ it("renders a toggle button for a 2-theme pair", () => {
27
+ setup(<ThemeSwitcher themes={["light", "dark"]} />);
28
+ expect(screen.getByRole("button", { name: /theme:/i })).toBeInTheDocument();
29
+ });
30
+
31
+ it("cycles the theme on click (jsdom has no startViewTransition → instant setTheme)", async () => {
32
+ setup(<ThemeSwitcher themes={["light", "dark"]} showSystem={false} />);
33
+ await userEvent.click(screen.getByRole("button", { name: /theme:/i }));
34
+ expect(document.documentElement.getAttribute("data-theme")).toBe("dark");
35
+ });
36
+
37
+ // The >2-themes AUTO-upgrade can't be exercised while only two themes ship
38
+ // (the reference registry ships two), so this pins the explicit dropdown mode —
39
+ // the same render path the auto-upgrade selects.
40
+ it("renders a dropdown trigger in dropdown mode", () => {
41
+ setup(<ThemeSwitcher mode="dropdown" themes={[...BUILT_IN_THEMES]} />);
42
+ expect(screen.getByRole("button", { name: "Theme" })).toBeInTheDocument();
43
+ });
44
+ });
45
+
46
+ describe("ThemeSwitcher — controlled preference (#366)", () => {
47
+ it("renders the current preference from the caller's state and reports every pick, writing no localStorage", async () => {
48
+ const onPreferenceChange = vi.fn();
49
+ setup(
50
+ <ThemeSwitcher
51
+ themes={["light", "dark"]}
52
+ showSystem={false}
53
+ preference="light"
54
+ onPreferenceChange={onPreferenceChange}
55
+ />,
56
+ );
57
+ const btn = screen.getByRole("button", { name: /theme: light/i });
58
+ await userEvent.click(btn);
59
+ expect(onPreferenceChange).toHaveBeenCalledTimes(1);
60
+ expect(onPreferenceChange).toHaveBeenCalledWith("dark");
61
+ expect(window.localStorage.getItem(SYSTEM_STORAGE_KEY)).toBeNull();
62
+ });
63
+
64
+ it('picking a theme from a "system" preference reports the resolved ThemeName, not "system"', async () => {
65
+ const onPreferenceChange = vi.fn();
66
+ setup(
67
+ <ThemeSwitcher
68
+ themes={["light", "dark"]}
69
+ preference="system"
70
+ onPreferenceChange={onPreferenceChange}
71
+ />,
72
+ );
73
+ // order is [light, dark, system]; "system" cycles to "light".
74
+ const btn = screen.getByRole("button", { name: /theme: system/i });
75
+ await userEvent.click(btn);
76
+ expect(onPreferenceChange).toHaveBeenCalledTimes(1);
77
+ expect(onPreferenceChange).toHaveBeenCalledWith("light");
78
+ expect(window.localStorage.getItem(SYSTEM_STORAGE_KEY)).toBeNull();
79
+ });
80
+
81
+ it("the view-transition setter still fires in controlled mode (the theme visibly applies)", async () => {
82
+ const onPreferenceChange = vi.fn();
83
+ setup(
84
+ <ThemeSwitcher
85
+ themes={["light", "dark"]}
86
+ showSystem={false}
87
+ preference="light"
88
+ onPreferenceChange={onPreferenceChange}
89
+ />,
90
+ );
91
+ await userEvent.click(screen.getByRole("button", { name: /theme: light/i }));
92
+ expect(document.documentElement.getAttribute("data-theme")).toBe("dark");
93
+ });
94
+
95
+ it("uncontrolled usage (no preference prop) is unchanged: it still tracks its own localStorage key", async () => {
96
+ setup(<ThemeSwitcher themes={["light", "dark"]} showSystem={false} />);
97
+ await userEvent.click(screen.getByRole("button", { name: /theme:/i }));
98
+ expect(document.documentElement.getAttribute("data-theme")).toBe("dark");
99
+ expect(window.localStorage.getItem(SYSTEM_STORAGE_KEY)).toBe("0");
100
+ });
101
+ });
102
+
103
+ describe("ThemeSwitcher — provider-scoped themes (#384)", () => {
104
+ // The restriction must be tested against a registry BIGGER than the subset,
105
+ // or the assertion is vacuous: the two reference themes ARE the allowed list,
106
+ // so a registry of exactly those two would prove nothing about narrowing.
107
+ const drafting = defineTheme({ value: "drafting", label: "Drafting", dark: false });
108
+ const REGISTRY = [...BUILT_IN_THEME_DEFINITIONS, drafting];
109
+ const ALL_NAMES = REGISTRY.map((t) => t.value);
110
+
111
+ it("never renders a theme the provider disallows, even when the themes prop lists it", async () => {
112
+ render(
113
+ <ThemeProvider themes={REGISTRY} allowedThemes={["light", "dark"]}>
114
+ <ThemeSwitcher mode="dropdown" themes={ALL_NAMES} />
115
+ </ThemeProvider>,
116
+ );
117
+ await userEvent.click(screen.getByRole("button", { name: "Theme" }));
118
+ const menu = await screen.findByRole("menu");
119
+ expect(within(menu).queryByText(/drafting/i)).not.toBeInTheDocument();
120
+ expect(within(menu).getByText("Light")).toBeInTheDocument();
121
+ expect(within(menu).getByText("Dark")).toBeInTheDocument();
122
+ });
123
+
124
+ it("no rendered control ever applies the disallowed theme (menu items + System)", async () => {
125
+ render(
126
+ <ThemeProvider themes={REGISTRY} allowedThemes={["light", "dark"]}>
127
+ <ThemeSwitcher mode="dropdown" themes={ALL_NAMES} />
128
+ </ThemeProvider>,
129
+ );
130
+ await userEvent.click(screen.getByRole("button", { name: "Theme" }));
131
+ const darkItem = await screen.findByRole("menuitem", { name: /dark/i });
132
+ await userEvent.click(darkItem);
133
+ expect(document.documentElement.getAttribute("data-theme")).toBe("dark");
134
+
135
+ await userEvent.click(screen.getByRole("button", { name: "Theme" }));
136
+ const systemItem = await screen.findByRole("menuitem", { name: /system/i });
137
+ await userEvent.click(systemItem);
138
+ expect(["light", "dark"]).toContain(document.documentElement.getAttribute("data-theme"));
139
+ });
140
+
141
+ it("an empty intersection (themes prop entirely disallowed) falls back to the provider's list, never the disallowed prop", async () => {
142
+ render(
143
+ <ThemeProvider allowedThemes={["light"]}>
144
+ <ThemeSwitcher mode="dropdown" themes={["dark"]} />
145
+ </ThemeProvider>,
146
+ );
147
+ await userEvent.click(screen.getByRole("button", { name: "Theme" }));
148
+ const menu = await screen.findByRole("menu");
149
+ expect(within(menu).queryByText("Dark")).not.toBeInTheDocument();
150
+ expect(within(menu).getByText("Light")).toBeInTheDocument();
151
+ });
152
+
153
+ it("BACKWARD COMPAT: a default <ThemeSwitcher /> under a non-restricting provider renders exactly today's toggle, not a dropdown", () => {
154
+ render(
155
+ <ThemeProvider>
156
+ <ThemeSwitcher />
157
+ </ThemeProvider>,
158
+ );
159
+ // The default 2-theme toggle presentation — never upgraded to a dropdown by
160
+ // provider.themes (which is the full built-in registry when no
161
+ // allowedThemes is set).
162
+ expect(screen.getByRole("button", { name: /theme:/i })).toBeInTheDocument();
163
+ expect(screen.queryByRole("menu")).not.toBeInTheDocument();
164
+ });
165
+
166
+ it("BACKWARD COMPAT: an explicit 2-theme pair under a non-restricting provider is unaffected", async () => {
167
+ render(
168
+ <ThemeProvider>
169
+ <ThemeSwitcher themes={["light", "dark"]} showSystem={false} />
170
+ </ThemeProvider>,
171
+ );
172
+ await userEvent.click(screen.getByRole("button", { name: /theme:/i }));
173
+ expect(document.documentElement.getAttribute("data-theme")).toBe("dark");
174
+ });
175
+ });
176
+
177
+ /**
178
+ * The switcher renders the PROVIDER's registry (ADR 0029). Before this it
179
+ * defaulted to a hard-coded `["light","dark"]` pair, so an app that registered
180
+ * its own themes got a switcher that could not reach them — and, worse, a
181
+ * switcher whose `light`/`dark` fallbacks named themes that app may not ship at
182
+ * all. These drive consumer theme names end to end: label, iconography, the
183
+ * auto-upgrade to a dropdown, and what "System" resolves to.
184
+ */
185
+ describe("ThemeSwitcher — consumer themes (ADR 0029)", () => {
186
+ const midnight = defineTheme({ value: "midnight", label: "Midnight", dark: true });
187
+ const parchment = defineTheme({ value: "parchment", label: "Parchment", dark: false });
188
+
189
+ it("offers the provider's consumer themes with NO themes prop at all", async () => {
190
+ render(
191
+ <ThemeProvider themes={[parchment, midnight]} defaultTheme="parchment">
192
+ <ThemeSwitcher mode="dropdown" />
193
+ </ThemeProvider>,
194
+ );
195
+ await userEvent.click(screen.getByRole("button", { name: "Theme" }));
196
+ const menu = await screen.findByRole("menu");
197
+ expect(within(menu).getByText("Parchment")).toBeInTheDocument();
198
+ expect(within(menu).getByText("Midnight")).toBeInTheDocument();
199
+ // The old hard-coded default pair is not silently in the list.
200
+ expect(within(menu).queryByText("Light")).not.toBeInTheDocument();
201
+ });
202
+
203
+ it("applies a consumer theme when picked", async () => {
204
+ render(
205
+ <ThemeProvider themes={[parchment, midnight]} defaultTheme="parchment">
206
+ <ThemeSwitcher mode="dropdown" />
207
+ </ThemeProvider>,
208
+ );
209
+ await userEvent.click(screen.getByRole("button", { name: "Theme" }));
210
+ await userEvent.click(await screen.findByRole("menuitem", { name: /midnight/i }));
211
+ expect(document.documentElement.getAttribute("data-theme")).toBe("midnight");
212
+ });
213
+
214
+ it("auto-upgrades to a dropdown once the registry passes two themes", async () => {
215
+ render(
216
+ <ThemeProvider themes={[...BUILT_IN_THEME_DEFINITIONS, midnight]}>
217
+ <ThemeSwitcher />
218
+ </ThemeProvider>,
219
+ );
220
+ // Three registered themes → the dropdown trigger, not the cycle toggle.
221
+ expect(screen.getByRole("button", { name: "Theme" })).toBeInTheDocument();
222
+ expect(screen.queryByRole("button", { name: /theme:/i })).not.toBeInTheDocument();
223
+ });
224
+
225
+ it("announces the consumer theme's OWN label in toggle mode, never a name the app lacks", async () => {
226
+ render(
227
+ <ThemeProvider themes={[parchment, midnight]} defaultTheme="parchment">
228
+ <ThemeSwitcher showSystem={false} />
229
+ </ThemeProvider>,
230
+ );
231
+ // The pre-0029 code labelled the roles ("Theme: light"), naming a theme this
232
+ // app does not ship.
233
+ const button = screen.getByRole("button", { name: /theme:/i });
234
+ expect(button).toHaveAccessibleName(/Theme: Parchment\. Activate to switch to Midnight\./i);
235
+ await userEvent.click(button);
236
+ expect(document.documentElement.getAttribute("data-theme")).toBe("midnight");
237
+ });
238
+
239
+ it('resolves "System" to a registered theme of the right darkness, not to "light"/"dark"', async () => {
240
+ render(
241
+ <ThemeProvider themes={[parchment, midnight]} defaultTheme="parchment">
242
+ <ThemeSwitcher mode="dropdown" />
243
+ </ThemeProvider>,
244
+ );
245
+ await userEvent.click(screen.getByRole("button", { name: "Theme" }));
246
+ await userEvent.click(await screen.findByRole("menuitem", { name: /system/i }));
247
+ // jsdom's matchMedia stub reports no dark preference, so the light-side
248
+ // registry entry wins — the point is that it is a REGISTERED name.
249
+ expect(["parchment", "midnight"]).toContain(
250
+ document.documentElement.getAttribute("data-theme"),
251
+ );
252
+ });
253
+ });