@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,121 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Globe, Home, Settings, Sparkles } from "lucide-react";
3
+ import {
4
+ SidebarGroup,
5
+ SidebarGroupContent,
6
+ SidebarInset,
7
+ SidebarMenu,
8
+ SidebarMenuButton,
9
+ SidebarMenuItem,
10
+ SidebarProvider,
11
+ } from "../sidebar";
12
+ import { NavUser } from "../nav-user";
13
+ import { TeamSwitcher } from "../team-switcher";
14
+ import { AppSidebar } from "./app-sidebar";
15
+
16
+ const meta = {
17
+ title: "Layout/AppSidebar",
18
+ component: AppSidebar,
19
+ tags: ["autodocs"],
20
+ parameters: { layout: "fullscreen" },
21
+ decorators: [
22
+ (Story) => (
23
+ <SidebarProvider>
24
+ <div className="flex h-dvh w-full">
25
+ <Story />
26
+ <SidebarInset className="flex flex-col p-6 text-body text-muted-foreground">
27
+ Main content area
28
+ </SidebarInset>
29
+ </div>
30
+ </SidebarProvider>
31
+ ),
32
+ ],
33
+ } satisfies Meta<typeof AppSidebar>;
34
+ export default meta;
35
+ type Story = StoryObj<typeof meta>;
36
+
37
+ const sampleTeams = [
38
+ { name: "Acme Inc.", logo: Sparkles, plan: "Enterprise" },
39
+ { name: "Beta Corp.", logo: Globe, plan: "Free" },
40
+ ];
41
+
42
+ const sampleUser = { name: "Jane Doe", email: "jane@example.com" };
43
+
44
+ const navItems = [
45
+ { title: "Home", icon: Home, url: "#" },
46
+ { title: "Settings", icon: Settings, url: "#" },
47
+ ];
48
+
49
+ export const Default: Story = {
50
+ render: () => (
51
+ <AppSidebar
52
+ header={<TeamSwitcher teams={sampleTeams} />}
53
+ footer={<NavUser user={sampleUser} />}
54
+ >
55
+ <SidebarGroup>
56
+ <SidebarGroupContent>
57
+ <SidebarMenu>
58
+ {navItems.map((item) => (
59
+ <SidebarMenuItem key={item.title}>
60
+ <SidebarMenuButton asChild>
61
+ <a href={item.url}>
62
+ <item.icon />
63
+ <span>{item.title}</span>
64
+ </a>
65
+ </SidebarMenuButton>
66
+ </SidebarMenuItem>
67
+ ))}
68
+ </SidebarMenu>
69
+ </SidebarGroupContent>
70
+ </SidebarGroup>
71
+ </AppSidebar>
72
+ ),
73
+ };
74
+
75
+ export const HeaderOnly: Story = {
76
+ name: "Header only (no footer)",
77
+ render: () => (
78
+ <AppSidebar header={<TeamSwitcher teams={sampleTeams} />}>
79
+ <SidebarGroup>
80
+ <SidebarGroupContent>
81
+ <SidebarMenu>
82
+ {navItems.map((item) => (
83
+ <SidebarMenuItem key={item.title}>
84
+ <SidebarMenuButton asChild>
85
+ <a href={item.url}>
86
+ <item.icon />
87
+ <span>{item.title}</span>
88
+ </a>
89
+ </SidebarMenuButton>
90
+ </SidebarMenuItem>
91
+ ))}
92
+ </SidebarMenu>
93
+ </SidebarGroupContent>
94
+ </SidebarGroup>
95
+ </AppSidebar>
96
+ ),
97
+ };
98
+
99
+ export const FooterOnly: Story = {
100
+ name: "Footer only (no header)",
101
+ render: () => (
102
+ <AppSidebar footer={<NavUser user={sampleUser} />}>
103
+ <SidebarGroup>
104
+ <SidebarGroupContent>
105
+ <SidebarMenu>
106
+ {navItems.map((item) => (
107
+ <SidebarMenuItem key={item.title}>
108
+ <SidebarMenuButton asChild>
109
+ <a href={item.url}>
110
+ <item.icon />
111
+ <span>{item.title}</span>
112
+ </a>
113
+ </SidebarMenuButton>
114
+ </SidebarMenuItem>
115
+ ))}
116
+ </SidebarMenu>
117
+ </SidebarGroupContent>
118
+ </SidebarGroup>
119
+ </AppSidebar>
120
+ ),
121
+ };
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { SidebarProvider } from "../sidebar";
4
+ import { AppSidebar } from "./app-sidebar";
5
+
6
+ function Wrapper({ children }: { children: React.ReactNode }) {
7
+ return <SidebarProvider>{children}</SidebarProvider>;
8
+ }
9
+
10
+ describe("AppSidebar", () => {
11
+ it("renders children in the content slot", () => {
12
+ render(
13
+ <Wrapper>
14
+ <AppSidebar>
15
+ <div>nav content</div>
16
+ </AppSidebar>
17
+ </Wrapper>,
18
+ );
19
+ expect(screen.getByText("nav content")).toBeInTheDocument();
20
+ });
21
+
22
+ it("renders header slot when provided", () => {
23
+ render(
24
+ <Wrapper>
25
+ <AppSidebar header={<div>sidebar header</div>}>
26
+ <div>nav</div>
27
+ </AppSidebar>
28
+ </Wrapper>,
29
+ );
30
+ expect(screen.getByText("sidebar header")).toBeInTheDocument();
31
+ });
32
+
33
+ it("renders footer slot when provided", () => {
34
+ render(
35
+ <Wrapper>
36
+ <AppSidebar footer={<div>sidebar footer</div>}>
37
+ <div>nav</div>
38
+ </AppSidebar>
39
+ </Wrapper>,
40
+ );
41
+ expect(screen.getByText("sidebar footer")).toBeInTheDocument();
42
+ });
43
+
44
+ it("omits header DOM node when header prop is not provided", () => {
45
+ render(
46
+ <Wrapper>
47
+ <AppSidebar footer={<div>footer</div>}>
48
+ <div>nav</div>
49
+ </AppSidebar>
50
+ </Wrapper>,
51
+ );
52
+ expect(screen.getByText("footer")).toBeInTheDocument();
53
+ expect(screen.getByText("nav")).toBeInTheDocument();
54
+ });
55
+ });
@@ -0,0 +1,53 @@
1
+ "use client";
2
+
3
+ import { forwardRef, type ComponentProps, type ReactNode } from "react";
4
+ import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader } from "../sidebar";
5
+
6
+ export interface AppSidebarProps extends ComponentProps<typeof Sidebar> {
7
+ /**
8
+ * Content rendered inside `<SidebarHeader>`. Typically a `TeamSwitcher`
9
+ * or a logo + title combination.
10
+ */
11
+ header?: ReactNode;
12
+ /**
13
+ * Content rendered inside `<SidebarFooter>`. Typically a `NavUser` or
14
+ * a `TeamSwitcher` placed at the bottom.
15
+ */
16
+ footer?: ReactNode;
17
+ /**
18
+ * Main navigation content rendered inside `<SidebarContent>`. Pass one or
19
+ * more `<SidebarGroup>` / `<SidebarMenu>` compositions here.
20
+ */
21
+ children?: ReactNode;
22
+ }
23
+
24
+ /**
25
+ * Parameterized application sidebar shell. Composes `@radix-ui` `Sidebar`
26
+ * primitives with typed `header`, `footer`, and `children` slots.
27
+ *
28
+ * Registry blocks (`sidebar-02`, `sidebar-04`, `sidebar-05`) should import
29
+ * this instead of constructing the `Sidebar` → `SidebarHeader` →
30
+ * `SidebarContent` → `SidebarFooter` skeleton themselves.
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * <AppSidebar
35
+ * header={<TeamSwitcher teams={teams} />}
36
+ * footer={<NavUser user={currentUser} />}
37
+ * >
38
+ * <SidebarGroup>…</SidebarGroup>
39
+ * </AppSidebar>
40
+ * ```
41
+ */
42
+ export const AppSidebar = forwardRef<HTMLDivElement, AppSidebarProps>(function AppSidebar(
43
+ { header, footer, children, ...props },
44
+ ref,
45
+ ) {
46
+ return (
47
+ <Sidebar ref={ref} {...props}>
48
+ {header != null && <SidebarHeader>{header}</SidebarHeader>}
49
+ <SidebarContent>{children}</SidebarContent>
50
+ {footer != null && <SidebarFooter>{footer}</SidebarFooter>}
51
+ </Sidebar>
52
+ );
53
+ });
@@ -0,0 +1 @@
1
+ export { AppSidebar, type AppSidebarProps } from "./app-sidebar";
@@ -0,0 +1,46 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { AspectRatio } from "./aspect-ratio";
3
+ const meta = {
4
+ title: "Display/AspectRatio",
5
+ component: AspectRatio,
6
+ tags: ["autodocs"],
7
+ argTypes: {
8
+ ratio: {
9
+ description: "Aspect ratio as a number (e.g. `16/9`, `4/3`, `1`). Defaults to `1`.",
10
+ control: "number",
11
+ table: { category: "Appearance" },
12
+ },
13
+ className: {
14
+ description: "Extra Tailwind classes merged on the wrapper.",
15
+ control: "text",
16
+ table: { category: "Appearance" },
17
+ },
18
+ },
19
+ } satisfies Meta<typeof AspectRatio>;
20
+ export default meta;
21
+ type Story = StoryObj<typeof meta>;
22
+ export const Default: Story = {
23
+ render: () => (
24
+ <div className="w-72">
25
+ <AspectRatio
26
+ ratio={16 / 9}
27
+ className="flex items-center justify-center rounded-md bg-muted text-body text-muted-foreground"
28
+ >
29
+ 16 / 9
30
+ </AspectRatio>
31
+ </div>
32
+ ),
33
+ };
34
+
35
+ export const Square: Story = {
36
+ render: () => (
37
+ <div className="w-48">
38
+ <AspectRatio
39
+ ratio={1}
40
+ className="flex items-center justify-center rounded-md bg-muted text-body text-muted-foreground"
41
+ >
42
+ 1 / 1
43
+ </AspectRatio>
44
+ </div>
45
+ ),
46
+ };
@@ -0,0 +1,3 @@
1
+ import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
2
+
3
+ export const AspectRatio = AspectRatioPrimitive.Root;
@@ -0,0 +1 @@
1
+ export { AspectRatio } from "./aspect-ratio";
@@ -0,0 +1,83 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { AttributionPanel } from "./attribution-panel";
3
+
4
+ const meta = {
5
+ title: "Core/AttributionPanel",
6
+ component: AttributionPanel,
7
+ tags: ["autodocs"],
8
+ parameters: {
9
+ docs: {
10
+ description: {
11
+ component:
12
+ "The one surface where brand-ui's third-party notices are shown. Its content is " +
13
+ "GENERATED from the repo (`pnpm gen:attributions`, gated by `pnpm attributions:check`), " +
14
+ "so it cannot drift from what actually ships.\n\n" +
15
+ "**Why it matters:** `MapCanvas` disables MapLibre's in-map attribution control by " +
16
+ "default and the flow canvases hide the React Flow badge. The React Flow badge is an " +
17
+ "MIT project's request, but the default Carto basemap's OpenStreetMap data is " +
18
+ "ODbL-licensed and the credit is REQUIRED — mount this panel somewhere reachable " +
19
+ "(About dialog, settings pane, footer link) to keep that obligation met.",
20
+ },
21
+ },
22
+ },
23
+ argTypes: {
24
+ categories: {
25
+ description: "Restrict to these categories, in the panel's own order.",
26
+ table: { category: "Content" },
27
+ },
28
+ packages: {
29
+ description:
30
+ "Restrict to notices reachable through these packages — an app that never renders a map need not credit the basemap providers.",
31
+ table: { category: "Content" },
32
+ },
33
+ requiredOnly: {
34
+ description: "Show only notices a licence obliges the product to display.",
35
+ control: "boolean",
36
+ table: { category: "Content" },
37
+ },
38
+ maxHeight: {
39
+ description: "Cap the body height and scroll inside it.",
40
+ table: { category: "Layout" },
41
+ },
42
+ className: {
43
+ description: "Extra Tailwind classes merged via cn().",
44
+ control: "text",
45
+ table: { category: "Appearance" },
46
+ },
47
+ },
48
+ } satisfies Meta<typeof AttributionPanel>;
49
+
50
+ export default meta;
51
+ type Story = StoryObj<typeof meta>;
52
+
53
+ /** The full generated dataset, scrolling inside a fixed height. */
54
+ export const Default: Story = {
55
+ args: { maxHeight: 420, className: "max-w-2xl" },
56
+ };
57
+
58
+ /**
59
+ * The minimum a product must display: only the notices a licence obliges. This is
60
+ * the variant to mount when screen space is tight — a footer "Attributions" link
61
+ * or an About dialog section.
62
+ */
63
+ export const RequiredOnly: Story = {
64
+ args: { requiredOnly: true, className: "max-w-2xl" },
65
+ };
66
+
67
+ /** Scoped to one package — what an app that only renders maps has to credit. */
68
+ export const ScopedToMaps: Story = {
69
+ args: {
70
+ packages: ["@elabs-ai/components-maps"],
71
+ className: "max-w-2xl",
72
+ },
73
+ };
74
+
75
+ /** Adapted source and fonts only — the provenance half, without the dependency tail. */
76
+ export const SourceAndFonts: Story = {
77
+ args: { categories: ["source", "font"], className: "max-w-2xl" },
78
+ };
79
+
80
+ /** A filter that matches nothing still renders a real empty state, never a blank box. */
81
+ export const Empty: Story = {
82
+ args: { attributions: [], className: "max-w-2xl" },
83
+ };
@@ -0,0 +1,123 @@
1
+ import { cleanup, render, screen, within } from "@testing-library/react";
2
+ import { afterEach, describe, expect, it } from "vitest";
3
+ import { AttributionPanel } from "./attribution-panel";
4
+ import type { Attribution } from "./attribution-types";
5
+
6
+ afterEach(cleanup);
7
+
8
+ const fixture: Attribution[] = [
9
+ {
10
+ id: "openstreetmap",
11
+ category: "data",
12
+ name: "OpenStreetMap",
13
+ version: null,
14
+ license: "ODbL-1.0",
15
+ copyright: "© OpenStreetMap contributors",
16
+ url: "https://www.openstreetmap.org/copyright",
17
+ usedBy: ["@elabs-ai/components-maps"],
18
+ required: true,
19
+ note: "Map data behind the default basemap.",
20
+ },
21
+ {
22
+ id: "clsx",
23
+ category: "dependency",
24
+ name: "clsx",
25
+ version: "2.1.1",
26
+ license: "MIT",
27
+ copyright: "Luke Edwards",
28
+ url: "https://github.com/lukeed/clsx",
29
+ usedBy: ["@elabs-ai/components-ui"],
30
+ required: false,
31
+ note: null,
32
+ },
33
+ ];
34
+
35
+ describe("AttributionPanel", () => {
36
+ it("renders each attribution with its licence and copyright", () => {
37
+ render(<AttributionPanel attributions={fixture} />);
38
+ expect(screen.getByText("OpenStreetMap")).toBeInTheDocument();
39
+ expect(screen.getByText("© OpenStreetMap contributors")).toBeInTheDocument();
40
+ expect(screen.getByText("ODbL-1.0")).toBeInTheDocument();
41
+ expect(screen.getByText("clsx")).toBeInTheDocument();
42
+ });
43
+
44
+ // The obligation this component exists to meet: a required notice must be
45
+ // distinguishable by TEXT, not by colour alone (WCAG 1.4.1) — asserting the
46
+ // word means a future restyle can't reduce it to a coloured dot.
47
+ it("marks a licence-required notice with a word, not only a colour", () => {
48
+ render(<AttributionPanel attributions={fixture} />);
49
+ const osm = screen.getByText("OpenStreetMap").closest("li")!;
50
+ expect(within(osm).getByText("Required")).toBeInTheDocument();
51
+ const clsx = screen.getByText("clsx").closest("li")!;
52
+ expect(within(clsx).queryByText("Required")).not.toBeInTheDocument();
53
+ });
54
+
55
+ it("groups by category under a heading", () => {
56
+ render(<AttributionPanel attributions={fixture} />);
57
+ expect(screen.getByRole("heading", { name: "Map data & tiles" })).toBeInTheDocument();
58
+ expect(screen.getByRole("heading", { name: "Open-source dependencies" })).toBeInTheDocument();
59
+ });
60
+
61
+ it("filters to the licence-required notices with requiredOnly", () => {
62
+ render(<AttributionPanel attributions={fixture} requiredOnly />);
63
+ expect(screen.getByText("OpenStreetMap")).toBeInTheDocument();
64
+ expect(screen.queryByText("clsx")).not.toBeInTheDocument();
65
+ });
66
+
67
+ it("filters by consuming package so an app credits only what it ships", () => {
68
+ render(<AttributionPanel attributions={fixture} packages={["@elabs-ai/components-ui"]} />);
69
+ expect(screen.getByText("clsx")).toBeInTheDocument();
70
+ expect(screen.queryByText("OpenStreetMap")).not.toBeInTheDocument();
71
+ });
72
+
73
+ // Two panels share a page in Storybook autodocs (every story renders together)
74
+ // and in any app that mounts a scoped panel beside a full one. Heading ids must
75
+ // stay unique or aria-labelledby points at the wrong node and axe flags it.
76
+ it("scopes section heading ids per instance", () => {
77
+ const { container } = render(
78
+ <>
79
+ <AttributionPanel attributions={fixture} />
80
+ <AttributionPanel attributions={fixture} />
81
+ </>,
82
+ );
83
+ const ids = [...container.querySelectorAll("h3[id]")].map((h) => h.id);
84
+ expect(ids).toHaveLength(4);
85
+ expect(new Set(ids).size).toBe(4);
86
+ for (const section of container.querySelectorAll('[data-slot="attribution-panel-section"]')) {
87
+ const labelledBy = section.getAttribute("aria-labelledby")!;
88
+ expect(section.querySelector(`#${CSS.escape(labelledBy)}`)).not.toBeNull();
89
+ }
90
+ });
91
+
92
+ it("renders an empty state rather than a blank panel", () => {
93
+ render(<AttributionPanel attributions={[]} />);
94
+ expect(screen.getByText("No attributions match this filter.")).toBeInTheDocument();
95
+ });
96
+
97
+ it("opens external links safely", () => {
98
+ render(<AttributionPanel attributions={fixture} />);
99
+ const link = screen.getByRole("link", { name: "OpenStreetMap" });
100
+ expect(link).toHaveAttribute("target", "_blank");
101
+ expect(link).toHaveAttribute("rel", "noopener noreferrer");
102
+ });
103
+
104
+ it("merges className and spreads props onto the root", () => {
105
+ const { container } = render(
106
+ <AttributionPanel attributions={fixture} className="my-panel" id="attrib" />,
107
+ );
108
+ const root = container.querySelector('[data-slot="attribution-panel"]')!;
109
+ expect(root).toHaveClass("my-panel");
110
+ expect(root).toHaveAttribute("id", "attrib");
111
+ });
112
+
113
+ // The real dataset is generated; if the generator ever emits nothing, the panel
114
+ // would render an innocuous empty state and the obligation would silently lapse.
115
+ it("ships a non-empty generated dataset including the required map notices", async () => {
116
+ const { ATTRIBUTIONS } = await import("./attributions.generated");
117
+ expect(ATTRIBUTIONS.length).toBeGreaterThan(0);
118
+ const required = ATTRIBUTIONS.filter((a) => a.required);
119
+ expect(required.some((a) => a.id === "openstreetmap")).toBe(true);
120
+ expect(required.some((a) => a.id === "carto")).toBe(true);
121
+ for (const entry of required) expect(entry.copyright).toBeTruthy();
122
+ });
123
+ });
@@ -0,0 +1,191 @@
1
+ import { forwardRef, useId, useMemo, type HTMLAttributes } from "react";
2
+ import { cn } from "../../lib/cn";
3
+ import { Badge } from "../badge";
4
+ import { ScrollArea } from "../scroll-area";
5
+ import { useLocale } from "../locale-provider";
6
+ import { ATTRIBUTIONS } from "./attributions.generated";
7
+ import type { Attribution, AttributionCategory } from "./attribution-types";
8
+
9
+ /**
10
+ * Render order + the heading each group gets. `data` first because those are the
11
+ * notices a licence obliges us to show; `dependency` last because it is the long
12
+ * tail nobody scrolls to but everybody must be able to reach.
13
+ */
14
+ const CATEGORY_ORDER: readonly AttributionCategory[] = ["data", "source", "font", "dependency"];
15
+
16
+ const CATEGORY_LABEL: Record<AttributionCategory, string> = {
17
+ data: "Map data & tiles",
18
+ source: "Adapted source",
19
+ font: "Fonts",
20
+ dependency: "Open-source dependencies",
21
+ };
22
+
23
+ export interface AttributionPanelProps extends HTMLAttributes<HTMLElement> {
24
+ /**
25
+ * The notices to render. Defaults to the GENERATED dataset
26
+ * (`scripts/gen-attributions.mjs`), which is what keeps this panel accurate
27
+ * without anyone maintaining a list — override only to scope or to test.
28
+ */
29
+ attributions?: readonly Attribution[];
30
+ /** Restrict to these categories, in the panel's own order. Default: all. */
31
+ categories?: readonly AttributionCategory[];
32
+ /**
33
+ * Restrict to notices reachable through these packages — e.g. an app that only
34
+ * uses `…-ui` need not credit MapLibre's basemap providers. Matched against
35
+ * each entry's `usedBy`. Default: all.
36
+ */
37
+ packages?: readonly string[];
38
+ /** Show only the notices a licence obliges us to display. */
39
+ requiredOnly?: boolean;
40
+ /**
41
+ * Cap the body height and scroll inside it. Omit to let the panel grow and
42
+ * leave scrolling to the container (a Dialog body, a settings pane).
43
+ */
44
+ maxHeight?: number | string;
45
+ }
46
+
47
+ /** Filter + group in one pass so the render is a plain map over sections. */
48
+ function groupAttributions(
49
+ attributions: readonly Attribution[],
50
+ {
51
+ categories,
52
+ packages,
53
+ requiredOnly,
54
+ }: Pick<AttributionPanelProps, "categories" | "packages" | "requiredOnly">,
55
+ ) {
56
+ const order = categories ?? CATEGORY_ORDER;
57
+ const visible = attributions.filter(
58
+ (a) =>
59
+ order.includes(a.category) &&
60
+ (!requiredOnly || a.required) &&
61
+ (!packages || a.usedBy.some((p) => packages.includes(p))),
62
+ );
63
+ return order
64
+ .map((category) => ({ category, items: visible.filter((a) => a.category === category) }))
65
+ .filter((group) => group.items.length > 0);
66
+ }
67
+
68
+ function AttributionItem({ item, requiredLabel }: { item: Attribution; requiredLabel: string }) {
69
+ return (
70
+ <li data-slot="attribution-panel-item" className="flex flex-col gap-0.5 py-2">
71
+ <div className="flex flex-wrap items-center gap-x-2 gap-y-1">
72
+ {item.url ? (
73
+ <a
74
+ href={item.url}
75
+ target="_blank"
76
+ rel="noopener noreferrer"
77
+ className="text-body font-medium underline underline-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-xs"
78
+ >
79
+ {item.name}
80
+ </a>
81
+ ) : (
82
+ <span className="text-body font-medium">{item.name}</span>
83
+ )}
84
+ {item.version ? (
85
+ <span className="text-meta text-muted-foreground tabular-nums">{item.version}</span>
86
+ ) : null}
87
+ <span className="text-meta text-muted-foreground">{item.license}</span>
88
+ {item.required ? (
89
+ // Not colour-only: the badge carries the word itself (WCAG 1.4.1).
90
+ <Badge variant="outline" className="text-meta">
91
+ {requiredLabel}
92
+ </Badge>
93
+ ) : null}
94
+ </div>
95
+ {item.copyright ? (
96
+ <p className="text-meta text-muted-foreground break-words">{item.copyright}</p>
97
+ ) : null}
98
+ {item.note ? <p className="text-meta text-muted-foreground">{item.note}</p> : null}
99
+ </li>
100
+ );
101
+ }
102
+
103
+ /**
104
+ * The single surface where brand-ui's third-party notices are displayed —
105
+ * OSS dependencies, adapted source, self-hosted fonts, and the map data/tile
106
+ * credits that a licence actually obliges the product to show.
107
+ *
108
+ * The content is GENERATED from the repo (`pnpm gen:attributions`, gated by
109
+ * `pnpm attributions:check`), so it cannot drift from what is actually shipped
110
+ * the way a hand-kept credits page does.
111
+ *
112
+ * **Why this exists:** `MapCanvas` disables MapLibre's in-map attribution
113
+ * control by default, and the flow canvases hide the React Flow badge. The
114
+ * React Flow badge is an MIT project's request and carries no condition, but the
115
+ * default Carto basemap's OpenStreetMap data is ODbL-licensed and the credit is
116
+ * REQUIRED — mounting this panel somewhere a user can reach (an About dialog, a
117
+ * settings pane, a footer link) is what keeps that obligation met while the map
118
+ * corner stays clean. Ship it, or turn the in-map control back on.
119
+ */
120
+ export const AttributionPanel = forwardRef<HTMLElement, AttributionPanelProps>(
121
+ function AttributionPanel(
122
+ {
123
+ attributions = ATTRIBUTIONS,
124
+ categories,
125
+ packages,
126
+ requiredOnly,
127
+ maxHeight,
128
+ className,
129
+ ...props
130
+ },
131
+ ref,
132
+ ) {
133
+ const { t } = useLocale();
134
+ // Instance-scoped so two panels on one page (Storybook autodocs renders every
135
+ // story together) can't emit the same heading id — duplicate ids break the
136
+ // aria-labelledby target and fail axe.
137
+ const baseId = useId();
138
+ const groups = useMemo(
139
+ () => groupAttributions(attributions, { categories, packages, requiredOnly }),
140
+ [attributions, categories, packages, requiredOnly],
141
+ );
142
+
143
+ const body = (
144
+ <div className="flex flex-col gap-6">
145
+ {groups.map(({ category, items }) => (
146
+ <section
147
+ key={category}
148
+ data-slot="attribution-panel-section"
149
+ aria-labelledby={`${baseId}-${category}`}
150
+ >
151
+ <h3 id={`${baseId}-${category}`} className="text-subtitle text-foreground mb-1">
152
+ {CATEGORY_LABEL[category]}
153
+ </h3>
154
+ <ul className="divide-y divide-border">
155
+ {items.map((item) => (
156
+ <AttributionItem
157
+ key={item.id}
158
+ item={item}
159
+ requiredLabel={t("ui.attributionPanel.required")}
160
+ />
161
+ ))}
162
+ </ul>
163
+ </section>
164
+ ))}
165
+ </div>
166
+ );
167
+
168
+ return (
169
+ <section
170
+ ref={ref}
171
+ data-slot="attribution-panel"
172
+ aria-label={t("ui.attributionPanel.label")}
173
+ className={cn("bg-card border border-border rounded-lg p-4", className)}
174
+ {...props}
175
+ >
176
+ {groups.length === 0 ? (
177
+ <p className="text-body text-muted-foreground">{t("ui.attributionPanel.empty")}</p>
178
+ ) : maxHeight === undefined ? (
179
+ body
180
+ ) : (
181
+ <ScrollArea style={{ maxHeight }} className="pe-3">
182
+ {body}
183
+ </ScrollArea>
184
+ )}
185
+ </section>
186
+ );
187
+ },
188
+ );
189
+
190
+ export { ATTRIBUTIONS, CATEGORY_LABEL };
191
+ export type { Attribution, AttributionCategory };