@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,80 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect } from "storybook/test";
3
+ import { Progress } from "./progress";
4
+ const meta = {
5
+ title: "Display/Progress",
6
+ component: Progress,
7
+ tags: ["autodocs"],
8
+ argTypes: {
9
+ value: {
10
+ description: "Current progress value (0–100). `null` / undefined = indeterminate.",
11
+ control: "number",
12
+ table: { category: "State" },
13
+ },
14
+ max: {
15
+ description: "Maximum value; defaults to 100.",
16
+ control: "number",
17
+ table: { category: "Behavior" },
18
+ },
19
+ variant: {
20
+ description:
21
+ "Indicator fill tone (#358) — reuses the StatusBadge/Alert vocabulary. Pair a non-default tone with `aria-valuetext` so the state isn't color-only.",
22
+ control: { type: "select" },
23
+ options: ["default", "success", "warning", "destructive"],
24
+ table: { category: "Appearance" },
25
+ },
26
+ className: {
27
+ description: "Extra Tailwind classes merged via cn() on the track.",
28
+ control: "text",
29
+ table: { category: "Appearance" },
30
+ },
31
+ },
32
+ } satisfies Meta<typeof Progress>;
33
+ export default meta;
34
+ type Story = StoryObj<typeof meta>;
35
+ export const Default: Story = { render: () => <Progress value={62} className="w-64" /> };
36
+ export const Full: Story = { render: () => <Progress value={100} className="w-64" /> };
37
+ export const Empty: Story = { render: () => <Progress value={0} className="w-64" /> };
38
+
39
+ /**
40
+ * All four tones (#358). Non-text contrast of the indicator fill against the
41
+ * `bg-muted` track (WCAG 1.4.11, ≥3:1), **measured in a real browser** on this
42
+ * story (`getComputedStyle` → canvas readback, 2026-08-02) rather than derived
43
+ * from the token literals:
44
+ *
45
+ * | tone | light | dark |
46
+ * | ----------- | ----------- | --------- |
47
+ * | default | 4.09:1 | 7.17:1 |
48
+ * | success | 5.19:1 | 9.07:1 |
49
+ * | warning | 4.44:1 | 7.88:1 |
50
+ * | destructive | 4.94:1 | 4.79:1 |
51
+ *
52
+ * All four clear 3:1 in both painted themes. `warning` used to be the hole in
53
+ * this table (1.88:1 in light); it was a token-value gap, not a component
54
+ * one, and #381 fixed it at the token — `--warning` is now a deep amber that
55
+ * clears 3:1 on every content surface.
56
+ *
57
+ * High decoration is deliberately NOT in this table: the decoration dial no
58
+ * longer re-inks controls at all (decoration paints backgrounds and chart
59
+ * fills only), so the indicator fill is the SAME painted token at every
60
+ * decoration level and this measurement holds across the whole dial.
61
+ */
62
+ export const Tones: Story = {
63
+ render: () => (
64
+ <div className="flex w-64 flex-col gap-3">
65
+ <Progress value={62} variant="default" aria-label="Default tone" />
66
+ <Progress value={62} variant="success" aria-label="Success tone" />
67
+ <Progress value={62} variant="warning" aria-label="Warning tone" />
68
+ <Progress
69
+ value={100}
70
+ variant="destructive"
71
+ aria-label="Destructive tone"
72
+ aria-valuetext="Exceeded — 120 of 100"
73
+ />
74
+ </div>
75
+ ),
76
+ play: async ({ canvas }) => {
77
+ const destructive = canvas.getByRole("progressbar", { name: "Destructive tone" });
78
+ await expect(destructive).toHaveAttribute("aria-valuetext", "Exceeded — 120 of 100");
79
+ },
80
+ };
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { Progress } from "./progress";
4
+ describe("Progress", () => {
5
+ it("exposes the value to AT", () => {
6
+ render(<Progress value={40} />);
7
+ expect(screen.getByRole("progressbar")).toHaveAttribute("aria-valuenow", "40");
8
+ });
9
+
10
+ // #358 — tone variant
11
+ it("defaults the indicator fill to bg-primary when no variant is passed", () => {
12
+ const { container } = render(<Progress value={40} />);
13
+ const indicator = container.querySelector('[role="progressbar"] > *');
14
+ expect(indicator).toHaveClass("bg-primary");
15
+ expect(indicator).not.toHaveClass("bg-destructive");
16
+ });
17
+
18
+ it.each(["success", "warning", "destructive"] as const)(
19
+ "renders the %s tone as the indicator fill and not bg-primary",
20
+ (variant) => {
21
+ const { container } = render(<Progress value={40} variant={variant} />);
22
+ const indicator = container.querySelector('[role="progressbar"] > *');
23
+ expect(indicator).toHaveClass(`bg-${variant}`);
24
+ expect(indicator).not.toHaveClass("bg-primary");
25
+ },
26
+ );
27
+
28
+ it("passes aria-valuetext through to the underlying progressbar (non-color signal)", () => {
29
+ render(<Progress value={100} variant="destructive" aria-valuetext="Exceeded — 100 of 100" />);
30
+ expect(screen.getByRole("progressbar")).toHaveAttribute(
31
+ "aria-valuetext",
32
+ "Exceeded — 100 of 100",
33
+ );
34
+ });
35
+ });
@@ -0,0 +1,50 @@
1
+ import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
2
+ import * as ProgressPrimitive from "@radix-ui/react-progress";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { cn } from "../../lib/cn";
5
+
6
+ /**
7
+ * Tone vocabulary reused from `StatusBadge`/`Alert` (success/warning/destructive
8
+ * on semantic tokens, #358) rather than reinvented. `default` keeps the fill
9
+ * byte-identical (`bg-primary`) when `variant` is unset.
10
+ */
11
+ export const progressIndicatorVariants = cva("size-full flex-1 transition-transform", {
12
+ variants: {
13
+ variant: {
14
+ default: "bg-primary",
15
+ success: "bg-success",
16
+ warning: "bg-warning",
17
+ destructive: "bg-destructive",
18
+ },
19
+ },
20
+ defaultVariants: { variant: "default" },
21
+ });
22
+
23
+ export interface ProgressProps
24
+ extends
25
+ ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>,
26
+ VariantProps<typeof progressIndicatorVariants> {}
27
+
28
+ /**
29
+ * Color must not be the only signal a guardrail tripped: set `aria-valuetext`
30
+ * (e.g. `"Exceeded — 120 of 100"`) alongside a non-`default` `variant` — it
31
+ * passes straight through to the underlying `role="progressbar"` element via
32
+ * `...props` (Radix's `ProgressPrimitive.Root` already supports it).
33
+ */
34
+ export const Progress = forwardRef<ElementRef<typeof ProgressPrimitive.Root>, ProgressProps>(
35
+ function Progress({ className, value, variant, ...props }, ref) {
36
+ return (
37
+ <ProgressPrimitive.Root
38
+ ref={ref}
39
+ value={value}
40
+ className={cn("relative h-2 w-full overflow-hidden rounded-full bg-muted", className)}
41
+ {...props}
42
+ >
43
+ <ProgressPrimitive.Indicator
44
+ className={progressIndicatorVariants({ variant })}
45
+ style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
46
+ />
47
+ </ProgressPrimitive.Root>
48
+ );
49
+ },
50
+ );
@@ -0,0 +1 @@
1
+ export { RadioGroup, RadioGroupItem } from "./radio-group";
@@ -0,0 +1,75 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect } from "storybook/test";
3
+ import { RadioGroup, RadioGroupItem } from "./radio-group";
4
+ import { Label } from "../label";
5
+ const meta = {
6
+ title: "Forms/RadioGroup",
7
+ component: RadioGroup,
8
+ tags: ["autodocs"],
9
+ argTypes: {
10
+ value: {
11
+ description: "Controlled selected value.",
12
+ control: "text",
13
+ table: { category: "State" },
14
+ },
15
+ defaultValue: {
16
+ description: "Uncontrolled initial selected value.",
17
+ control: "text",
18
+ table: { category: "State" },
19
+ },
20
+ orientation: {
21
+ description: "Layout direction of the radio items.",
22
+ control: { type: "radio" },
23
+ options: ["horizontal", "vertical"],
24
+ table: { category: "Appearance" },
25
+ },
26
+ disabled: {
27
+ description: "Disables all radio items in the group.",
28
+ control: "boolean",
29
+ table: { category: "State" },
30
+ },
31
+ required: {
32
+ description: "Marks the group as required in a form context.",
33
+ control: "boolean",
34
+ table: { category: "Behavior" },
35
+ },
36
+ onValueChange: {
37
+ description: "Called when the selected value changes.",
38
+ control: false,
39
+ table: { category: "Behavior" },
40
+ },
41
+ children: {
42
+ description: "RadioGroupItem elements.",
43
+ control: false,
44
+ table: { category: "Content" },
45
+ },
46
+ className: {
47
+ description: "Additional CSS classes applied to the root element.",
48
+ control: "text",
49
+ table: { category: "Appearance" },
50
+ },
51
+ },
52
+ } satisfies Meta<typeof RadioGroup>;
53
+ export default meta;
54
+ type Story = StoryObj<typeof meta>;
55
+ export const Default: Story = {
56
+ render: () => (
57
+ <RadioGroup defaultValue="a">
58
+ {["a", "b", "c"].map((v) => (
59
+ <div key={v} className="flex items-center gap-2">
60
+ <RadioGroupItem value={v} id={`r-${v}`} />
61
+ <Label htmlFor={`r-${v}`}>Option {v.toUpperCase()}</Label>
62
+ </div>
63
+ ))}
64
+ </RadioGroup>
65
+ ),
66
+ // "A" starts checked; clicking "B" shifts the selection.
67
+ play: async ({ canvas, userEvent }) => {
68
+ const radioA = canvas.getByRole("radio", { name: /option a/i });
69
+ await expect(radioA).toBeChecked();
70
+ const radioB = canvas.getByRole("radio", { name: /option b/i });
71
+ await userEvent.click(radioB);
72
+ await expect(radioB).toBeChecked();
73
+ await expect(radioA).not.toBeChecked();
74
+ },
75
+ };
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { RadioGroup, RadioGroupItem } from "./radio-group";
5
+ describe("RadioGroup", () => {
6
+ it("selects an item", async () => {
7
+ render(
8
+ <RadioGroup>
9
+ <RadioGroupItem value="a" aria-label="a" />
10
+ <RadioGroupItem value="b" aria-label="b" />
11
+ </RadioGroup>,
12
+ );
13
+ await userEvent.click(screen.getByRole("radio", { name: "b" }));
14
+ expect(screen.getByRole("radio", { name: "b" })).toBeChecked();
15
+ });
16
+ });
@@ -0,0 +1,33 @@
1
+ import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
2
+ import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
3
+ import { Circle } from "lucide-react";
4
+ import { cn } from "../../lib/cn";
5
+
6
+ export const RadioGroup = forwardRef<
7
+ ElementRef<typeof RadioGroupPrimitive.Root>,
8
+ ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
9
+ >(function RadioGroup({ className, ...props }, ref) {
10
+ return <RadioGroupPrimitive.Root ref={ref} className={cn("grid gap-2", className)} {...props} />;
11
+ });
12
+
13
+ export const RadioGroupItem = forwardRef<
14
+ ElementRef<typeof RadioGroupPrimitive.Item>,
15
+ ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
16
+ >(function RadioGroupItem({ className, ...props }, ref) {
17
+ return (
18
+ <RadioGroupPrimitive.Item
19
+ ref={ref}
20
+ className={cn(
21
+ "aspect-square size-4 rounded-full border border-input text-primary shadow-sm transition-colors duration-fast ease-standard",
22
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
23
+ "disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary",
24
+ className,
25
+ )}
26
+ {...props}
27
+ >
28
+ <RadioGroupPrimitive.Indicator className="flex items-center justify-center animate-in zoom-in-75 fade-in duration-fast ease-entrance">
29
+ <Circle className="size-2.5 fill-primary text-primary" />
30
+ </RadioGroupPrimitive.Indicator>
31
+ </RadioGroupPrimitive.Item>
32
+ );
33
+ });
@@ -0,0 +1 @@
1
+ export { Rating, type RatingProps } from "./rating";
@@ -0,0 +1,111 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { expect } from "storybook/test";
3
+ import { useState } from "react";
4
+ import { Rating } from "./rating";
5
+
6
+ const meta = {
7
+ title: "Forms/Rating",
8
+ component: Rating,
9
+ tags: ["autodocs"],
10
+ argTypes: {
11
+ size: {
12
+ description: "Visual size of each star.",
13
+ control: { type: "inline-radio" },
14
+ options: ["sm", "md", "lg"],
15
+ table: { category: "Appearance" },
16
+ },
17
+ value: {
18
+ description: "Controlled rating value (0..max).",
19
+ control: "number",
20
+ table: { category: "State" },
21
+ },
22
+ defaultValue: {
23
+ description: "Uncontrolled initial value.",
24
+ control: "number",
25
+ table: { category: "State" },
26
+ },
27
+ max: {
28
+ description: "Number of stars.",
29
+ control: "number",
30
+ table: { category: "Appearance" },
31
+ },
32
+ allowHalf: {
33
+ description: "Allow half-star (0.5) increments.",
34
+ control: "boolean",
35
+ table: { category: "Behavior" },
36
+ },
37
+ readOnly: {
38
+ description: "Render as a non-interactive display.",
39
+ control: "boolean",
40
+ table: { category: "State" },
41
+ },
42
+ disabled: {
43
+ description: "Disable interaction.",
44
+ control: "boolean",
45
+ table: { category: "State" },
46
+ },
47
+ onValueChange: {
48
+ description: "Called when the rating changes.",
49
+ control: false,
50
+ table: { category: "Behavior" },
51
+ },
52
+ "aria-label": {
53
+ description: "Accessible name for the rating group.",
54
+ control: "text",
55
+ table: { category: "Appearance" },
56
+ },
57
+ className: {
58
+ description: "Additional CSS classes applied to the root element.",
59
+ control: "text",
60
+ table: { category: "Appearance" },
61
+ },
62
+ },
63
+ } satisfies Meta<typeof Rating>;
64
+
65
+ export default meta;
66
+ type Story = StoryObj<typeof meta>;
67
+
68
+ export const Default: Story = {
69
+ args: { defaultValue: 3 },
70
+ // Clicks the 5th star and confirms it becomes checked.
71
+ play: async ({ canvas, userEvent }) => {
72
+ const star5 = canvas.getByRole("radio", { name: /5 stars/i });
73
+ await userEvent.click(star5);
74
+ await expect(star5).toHaveAttribute("aria-checked", "true");
75
+ },
76
+ };
77
+
78
+ export const Sizes: Story = {
79
+ render: () => (
80
+ <div className="flex flex-col gap-3">
81
+ <Rating defaultValue={3} size="sm" aria-label="Small rating" />
82
+ <Rating defaultValue={3} size="md" aria-label="Medium rating" />
83
+ <Rating defaultValue={3} size="lg" aria-label="Large rating" />
84
+ </div>
85
+ ),
86
+ };
87
+
88
+ export const HalfSteps: Story = {
89
+ args: { defaultValue: 2.5, allowHalf: true },
90
+ };
91
+
92
+ export const ReadOnly: Story = {
93
+ args: { value: 4, readOnly: true },
94
+ };
95
+
96
+ export const Disabled: Story = {
97
+ args: { defaultValue: 2, disabled: true },
98
+ };
99
+
100
+ export const Controlled: Story = {
101
+ render: () => {
102
+ // eslint-disable-next-line react-hooks/rules-of-hooks
103
+ const [value, setValue] = useState(3);
104
+ return (
105
+ <div className="flex flex-col gap-2">
106
+ <Rating value={value} onValueChange={setValue} aria-label="Controlled rating" />
107
+ <p className="text-muted-foreground text-body tabular-nums">Value: {value}</p>
108
+ </div>
109
+ );
110
+ },
111
+ };
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { Rating } from "./rating";
5
+
6
+ describe("Rating", () => {
7
+ it("renders `max` interactive stars as a radiogroup", () => {
8
+ render(<Rating defaultValue={0} max={5} aria-label="Quality" />);
9
+ expect(screen.getByRole("radiogroup", { name: "Quality" })).toBeInTheDocument();
10
+ expect(screen.getAllByRole("radio")).toHaveLength(5);
11
+ });
12
+
13
+ it("marks the current star as checked", () => {
14
+ render(<Rating value={3} aria-label="Quality" />);
15
+ expect(screen.getByRole("radio", { name: "3 stars" })).toHaveAttribute("aria-checked", "true");
16
+ expect(screen.getByRole("radio", { name: "2 stars" })).toHaveAttribute("aria-checked", "false");
17
+ });
18
+
19
+ it("calls onValueChange when a star is clicked", async () => {
20
+ const user = userEvent.setup();
21
+ const onValueChange = vi.fn();
22
+ render(<Rating defaultValue={0} onValueChange={onValueChange} aria-label="Quality" />);
23
+ await user.click(screen.getByRole("radio", { name: "4 stars" }));
24
+ expect(onValueChange).toHaveBeenCalledWith(4);
25
+ });
26
+
27
+ it("adjusts the value with arrow keys", async () => {
28
+ const user = userEvent.setup();
29
+ const onValueChange = vi.fn();
30
+ render(<Rating defaultValue={2} onValueChange={onValueChange} aria-label="Quality" />);
31
+ await user.tab(); // focus the roving tab stop
32
+ await user.keyboard("{ArrowRight}");
33
+ expect(onValueChange).toHaveBeenCalledWith(3);
34
+ });
35
+
36
+ it("renders readOnly as a non-interactive img with an accessible label", () => {
37
+ render(<Rating value={4} max={5} readOnly />);
38
+ expect(screen.getByRole("img", { name: "4 out of 5" })).toBeInTheDocument();
39
+ expect(screen.queryByRole("radio")).not.toBeInTheDocument();
40
+ });
41
+ });
@@ -0,0 +1,226 @@
1
+ // a2ui.exposed: yes
2
+ import {
3
+ forwardRef,
4
+ useCallback,
5
+ useRef,
6
+ useState,
7
+ type HTMLAttributes,
8
+ type KeyboardEvent,
9
+ type PointerEvent,
10
+ } from "react";
11
+ import { Star } from "lucide-react";
12
+ import { cva, type VariantProps } from "class-variance-authority";
13
+ import { cn } from "../../lib/cn";
14
+
15
+ const ratingVariants = cva("inline-flex items-center", {
16
+ variants: {
17
+ size: {
18
+ sm: "gap-0.5",
19
+ md: "gap-1",
20
+ lg: "gap-1.5",
21
+ },
22
+ },
23
+ defaultVariants: {
24
+ size: "md",
25
+ },
26
+ });
27
+
28
+ const STAR_SIZE: Record<NonNullable<VariantProps<typeof ratingVariants>["size"]>, string> = {
29
+ sm: "size-4",
30
+ md: "size-5",
31
+ lg: "size-6",
32
+ };
33
+
34
+ export interface RatingProps
35
+ extends
36
+ Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "defaultValue">,
37
+ VariantProps<typeof ratingVariants> {
38
+ /** Controlled value (0..max). */
39
+ value?: number;
40
+ /** Uncontrolled default value (0..max). Default 0. */
41
+ defaultValue?: number;
42
+ /** Called when the rating changes. */
43
+ onValueChange?: (value: number) => void;
44
+ /** Number of stars. Default 5. */
45
+ max?: number;
46
+ /** Allow half-star values (0.5 increments). Default false. */
47
+ allowHalf?: boolean;
48
+ /** Render as a non-interactive display of the value. */
49
+ readOnly?: boolean;
50
+ /** Disable interaction (interactive variant only). */
51
+ disabled?: boolean;
52
+ /** Accessible name for the rating group. Default "Rating". */
53
+ "aria-label"?: string;
54
+ }
55
+
56
+ function clamp(v: number, min: number, max: number): number {
57
+ return Math.min(max, Math.max(min, v));
58
+ }
59
+
60
+ /**
61
+ * Rating — a star rating control.
62
+ *
63
+ * Controlled via `value`/`onValueChange`; uncontrolled via `defaultValue`.
64
+ * Interactive mode is a `radiogroup` (keyboard ←/→ adjust by `allowHalf ? 0.5 : 1`,
65
+ * Home clears, End sets max). `readOnly` renders a non-interactive `role="img"`
66
+ * summary. Half-star clicks land on the left/right half of a star when `allowHalf`.
67
+ */
68
+ export const Rating = forwardRef<HTMLDivElement, RatingProps>(function Rating(
69
+ {
70
+ value: valueProp,
71
+ defaultValue = 0,
72
+ onValueChange,
73
+ max = 5,
74
+ allowHalf = false,
75
+ readOnly = false,
76
+ disabled = false,
77
+ size = "md",
78
+ className,
79
+ "aria-label": ariaLabel = "Rating",
80
+ ...props
81
+ },
82
+ ref,
83
+ ) {
84
+ const isControlled = valueProp !== undefined;
85
+ const [internalValue, setInternalValue] = useState<number>(defaultValue);
86
+ const [hoverValue, setHoverValue] = useState<number | null>(null);
87
+
88
+ const current = clamp(isControlled ? (valueProp ?? 0) : internalValue, 0, max);
89
+ const interactive = !readOnly && !disabled;
90
+ const display = interactive && hoverValue !== null ? hoverValue : current;
91
+
92
+ const starRefs = useRef<(HTMLButtonElement | null)[]>([]);
93
+
94
+ const commit = useCallback(
95
+ (next: number) => {
96
+ const clamped = clamp(next, 0, max);
97
+ if (!isControlled) setInternalValue(clamped);
98
+ onValueChange?.(clamped);
99
+ },
100
+ [isControlled, max, onValueChange],
101
+ );
102
+
103
+ const step = allowHalf ? 0.5 : 1;
104
+
105
+ const valueFromPointer = useCallback(
106
+ (e: PointerEvent<HTMLButtonElement>, index: number): number => {
107
+ if (!allowHalf) return index;
108
+ const rect = e.currentTarget.getBoundingClientRect();
109
+ const isLeftHalf = e.clientX - rect.left < rect.width / 2;
110
+ return isLeftHalf ? index - 0.5 : index;
111
+ },
112
+ [allowHalf],
113
+ );
114
+
115
+ const handleKeyDown = useCallback(
116
+ (e: KeyboardEvent<HTMLDivElement>) => {
117
+ if (!interactive) return;
118
+ let next = current;
119
+ switch (e.key) {
120
+ case "ArrowRight":
121
+ case "ArrowUp":
122
+ next = clamp(current + step, 0, max);
123
+ break;
124
+ case "ArrowLeft":
125
+ case "ArrowDown":
126
+ next = clamp(current - step, 0, max);
127
+ break;
128
+ case "Home":
129
+ next = 0;
130
+ break;
131
+ case "End":
132
+ next = max;
133
+ break;
134
+ default:
135
+ return;
136
+ }
137
+ e.preventDefault();
138
+ commit(next);
139
+ // Move focus to the star representing the new value (roving tabindex).
140
+ const focusIndex = Math.max(0, Math.ceil(next) - 1);
141
+ starRefs.current[focusIndex]?.focus();
142
+ },
143
+ [interactive, current, step, max, commit],
144
+ );
145
+
146
+ // Roving tabindex: the star at the current value (or the first) is the tab stop.
147
+ const tabStopIndex = current > 0 ? Math.ceil(current) - 1 : 0;
148
+
149
+ // Read-only / disabled: a single non-interactive summary.
150
+ if (!interactive) {
151
+ return (
152
+ <div
153
+ ref={ref}
154
+ role="img"
155
+ aria-label={`${current} out of ${max}`}
156
+ className={cn(ratingVariants({ size }), disabled && "opacity-50", className)}
157
+ {...props}
158
+ >
159
+ {Array.from({ length: max }, (_, i) => (
160
+ <StarGlyph key={i} fill={clamp(display - i, 0, 1)} sizeClass={STAR_SIZE[size ?? "md"]} />
161
+ ))}
162
+ </div>
163
+ );
164
+ }
165
+
166
+ return (
167
+ <div
168
+ ref={ref}
169
+ role="radiogroup"
170
+ aria-label={ariaLabel}
171
+ className={cn(ratingVariants({ size }), className)}
172
+ onKeyDown={handleKeyDown}
173
+ onPointerLeave={() => setHoverValue(null)}
174
+ {...props}
175
+ >
176
+ {Array.from({ length: max }, (_, i) => {
177
+ const index = i + 1;
178
+ const checked = Math.ceil(current) === index;
179
+ return (
180
+ <button
181
+ key={index}
182
+ ref={(el) => {
183
+ starRefs.current[i] = el;
184
+ }}
185
+ type="button"
186
+ role="radio"
187
+ aria-checked={checked}
188
+ aria-label={`${index} ${index === 1 ? "star" : "stars"}`}
189
+ tabIndex={i === tabStopIndex ? 0 : -1}
190
+ className="rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background"
191
+ onClick={(e) =>
192
+ commit(valueFromPointer(e as unknown as PointerEvent<HTMLButtonElement>, index))
193
+ }
194
+ onPointerMove={(e) => setHoverValue(valueFromPointer(e, index))}
195
+ >
196
+ <StarGlyph fill={clamp(display - i, 0, 1)} sizeClass={STAR_SIZE[size ?? "md"]} />
197
+ </button>
198
+ );
199
+ })}
200
+ </div>
201
+ );
202
+ });
203
+
204
+ interface StarGlyphProps {
205
+ /** Fill fraction 0..1 for this star. */
206
+ fill: number;
207
+ sizeClass: string;
208
+ }
209
+
210
+ /** A single star rendered with a partial fill overlay (supports half stars). */
211
+ function StarGlyph({ fill, sizeClass }: StarGlyphProps) {
212
+ return (
213
+ <span className={cn("relative inline-block", sizeClass)} aria-hidden="true">
214
+ <Star className={cn(sizeClass, "text-muted-foreground")} />
215
+ {fill > 0 && (
216
+ <span
217
+ className="absolute inset-0 overflow-hidden"
218
+ // Width is a layout value (the fill fraction), never a color.
219
+ style={{ width: `${fill * 100}%` }}
220
+ >
221
+ <Star className={cn(sizeClass, "fill-current text-warning")} />
222
+ </span>
223
+ )}
224
+ </span>
225
+ );
226
+ }
@@ -0,0 +1 @@
1
+ export { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "./resizable";