@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,637 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ChangeReview — AI-edit trust gate.
5
+ *
6
+ * When an agent proposes edits, the human reviews them hunk-by-hunk and accepts
7
+ * or rejects each before they apply. Feeling: calm, scannable, trustworthy —
8
+ * "I can see exactly what changed, who proposed it, and nothing lands without
9
+ * my say-so."
10
+ *
11
+ * Design references: GitHub PR file-review (per-hunk viewed/comment),
12
+ * Cursor/Copilot "review changes" panel, Graphite/Linear review UIs.
13
+ *
14
+ * State grid: empty (no hunks → StatePanel), single hunk, mixed, all-approved,
15
+ * all-rejected, very long hunk content. Approval state is signalled with color
16
+ * AND icon/label (colorblind + high-decoration/high-contrast safe).
17
+ *
18
+ * Compound structure (named exports, the Card/CardHeader convention — NOT dot
19
+ * notation). Use the `ChangeReview` root for the default layout, or compose the
20
+ * parts for a custom one:
21
+ * <ChangeReviewProvider> — lifted state (approved set + actions interface)
22
+ * <ChangeReviewHeader> — count summary + Approve all / Reject all
23
+ * <ChangeReviewProvenance> — who/what proposed (quiet attribution)
24
+ * <ChangeReviewList> — the hunk list
25
+ * <ChangeReviewHunk> — per-hunk row (title, rails, before/after, toggle)
26
+ */
27
+
28
+ import {
29
+ createContext,
30
+ forwardRef,
31
+ use,
32
+ useCallback,
33
+ useId,
34
+ useMemo,
35
+ useState,
36
+ type HTMLAttributes,
37
+ type ReactNode,
38
+ } from "react";
39
+ import { Check, X, CheckCheck, User, Bot, Clock } from "lucide-react";
40
+ import { cva, type VariantProps } from "class-variance-authority";
41
+ import { cn } from "../../lib/cn";
42
+ import { Button } from "../button/button";
43
+ import { Badge } from "../badge/badge";
44
+ import { StatePanel } from "../state-panel/state-panel";
45
+
46
+ // ─── Public types ─────────────────────────────────────────────────────────────
47
+
48
+ /** Who / what proposed the change set (presentational only; never fetches). */
49
+ export interface ChangeProvenance {
50
+ /** Display name of the person or agent author. */
51
+ author?: string;
52
+ /** Model or tool identifier (e.g. "claude-sonnet-4-6"). */
53
+ model?: string;
54
+ /** ISO-8601 or human-readable timestamp. */
55
+ timestamp?: string;
56
+ /** Free-form note, e.g. commit hash or run ID. */
57
+ note?: string;
58
+ }
59
+
60
+ /** Status of a single change hunk. Drives the accent-rail colour and icon. */
61
+ export type ChangeHunkStatus = "added" | "removed" | "modified";
62
+
63
+ /** A single proposed change in the review set. */
64
+ export interface ChangeHunk {
65
+ /** Unique identifier for this hunk (used in the approved set). */
66
+ id: string;
67
+ /** Short title shown in the hunk header (e.g. filename or section label). */
68
+ title?: string;
69
+ /** The content before the proposed edit. */
70
+ before?: ReactNode;
71
+ /** The content after the proposed edit (the proposal). */
72
+ after?: ReactNode;
73
+ /** Change type — drives accent rail + icon. @default "modified" */
74
+ status?: ChangeHunkStatus;
75
+ /** Per-hunk provenance override (overrides the review-level provenance). */
76
+ provenance?: ChangeProvenance;
77
+ }
78
+
79
+ /** State surface exposed by the ChangeReview context. */
80
+ export interface ChangeReviewState {
81
+ /** Set of currently-approved hunk IDs. */
82
+ approved: ReadonlySet<string>;
83
+ /** Total number of hunks. */
84
+ totalCount: number;
85
+ /** Number of approved hunks. */
86
+ approvedCount: number;
87
+ /** True when every hunk is approved. */
88
+ allApproved: boolean;
89
+ /** True when no hunk is approved. */
90
+ noneApproved: boolean;
91
+ }
92
+
93
+ /** Actions exposed by the ChangeReview context. */
94
+ export interface ChangeReviewActions {
95
+ /** Toggle the approval state of a single hunk. */
96
+ toggle: (id: string, next: boolean) => void;
97
+ /** Approve all hunks. */
98
+ approveAll: () => void;
99
+ /** Reject (un-approve) all hunks. */
100
+ rejectAll: () => void;
101
+ }
102
+
103
+ // ─── Context ──────────────────────────────────────────────────────────────────
104
+
105
+ interface ChangeReviewContextValue {
106
+ state: ChangeReviewState;
107
+ actions: ChangeReviewActions;
108
+ hunks: ChangeHunk[];
109
+ headingId: string;
110
+ }
111
+
112
+ const ChangeReviewContext = createContext<ChangeReviewContextValue | null>(null);
113
+
114
+ function useChangeReview(): ChangeReviewContextValue {
115
+ const ctx = use(ChangeReviewContext);
116
+ if (!ctx) {
117
+ throw new Error("ChangeReview sub-components must be rendered inside ChangeReview.Provider");
118
+ }
119
+ return ctx;
120
+ }
121
+
122
+ // ─── Variants ─────────────────────────────────────────────────────────────────
123
+
124
+ /**
125
+ * Hunk-status accent rail — the separation grammar's accent-rail channel: ONE
126
+ * focal gesture (the status rail) on a neutral card ground (`bg-card` + the
127
+ * subtle, redundant-exempt `border-border` hairline). NO colored wash and NO
128
+ * redundant colored border — the slop the "AI card" look is built from. Status
129
+ * reads from the rail + the header icon + the StatusBadge (never color alone).
130
+ * Approval is signalled by the StatusBadge + the filled toggle (no ring wash).
131
+ */
132
+ export const hunkVariants = cva(
133
+ [
134
+ "relative rounded-md border border-border bg-card text-card-foreground",
135
+ "transition-colors duration-fast ease-standard motion-reduce:transition-none",
136
+ ],
137
+ {
138
+ variants: {
139
+ status: {
140
+ added: "border-s-4 border-s-success",
141
+ removed: "border-s-4 border-s-destructive",
142
+ modified: "border-s-4 border-s-info",
143
+ },
144
+ },
145
+ defaultVariants: { status: "modified" },
146
+ },
147
+ );
148
+
149
+ // ─── Provider ─────────────────────────────────────────────────────────────────
150
+
151
+ export interface ChangeReviewProviderProps {
152
+ hunks: ChangeHunk[];
153
+ /**
154
+ * Controlled approved set. Pass `Set<string>` or a string array.
155
+ * When provided, the component is controlled and `onToggle` / `onApproveAll` /
156
+ * `onRejectAll` are the only way to update the state.
157
+ */
158
+ approved?: Set<string> | string[];
159
+ onToggle?: (id: string, next: boolean) => void;
160
+ onApproveAll?: () => void;
161
+ onRejectAll?: () => void;
162
+ children: ReactNode;
163
+ }
164
+
165
+ /**
166
+ * Lifts the approval state. Can be controlled (pass `approved`) or uncontrolled.
167
+ * Derive `isControlled = approved !== undefined`.
168
+ */
169
+ export function ChangeReviewProvider({
170
+ hunks,
171
+ approved: approvedProp,
172
+ onToggle,
173
+ onApproveAll,
174
+ onRejectAll,
175
+ children,
176
+ }: ChangeReviewProviderProps) {
177
+ const headingId = useId();
178
+
179
+ const isControlled = approvedProp !== undefined;
180
+
181
+ // Uncontrolled internal state.
182
+ const [internalApproved, setInternalApproved] = useState<Set<string>>(() => new Set());
183
+
184
+ const resolvedApproved: ReadonlySet<string> = isControlled
185
+ ? approvedProp instanceof Set
186
+ ? approvedProp
187
+ : new Set(approvedProp)
188
+ : internalApproved;
189
+
190
+ const hunkIds = useMemo(() => hunks.map((h) => h.id), [hunks]);
191
+
192
+ const toggle = useCallback(
193
+ (id: string, next: boolean) => {
194
+ onToggle?.(id, next);
195
+ if (!isControlled) {
196
+ setInternalApproved((prev) => {
197
+ const next_ = new Set(prev);
198
+ if (next) next_.add(id);
199
+ else next_.delete(id);
200
+ return next_;
201
+ });
202
+ }
203
+ },
204
+ [isControlled, onToggle],
205
+ );
206
+
207
+ const approveAll = useCallback(() => {
208
+ onApproveAll?.();
209
+ if (!isControlled) {
210
+ setInternalApproved(new Set(hunkIds));
211
+ }
212
+ }, [isControlled, hunkIds, onApproveAll]);
213
+
214
+ const rejectAll = useCallback(() => {
215
+ onRejectAll?.();
216
+ if (!isControlled) {
217
+ setInternalApproved(new Set());
218
+ }
219
+ }, [isControlled, onRejectAll]);
220
+
221
+ const approvedCount = hunkIds.filter((id) => resolvedApproved.has(id)).length;
222
+ const totalCount = hunkIds.length;
223
+
224
+ const state: ChangeReviewState = {
225
+ approved: resolvedApproved,
226
+ totalCount,
227
+ approvedCount,
228
+ allApproved: totalCount > 0 && approvedCount === totalCount,
229
+ noneApproved: approvedCount === 0,
230
+ };
231
+
232
+ const actions: ChangeReviewActions = { toggle, approveAll, rejectAll };
233
+
234
+ const value = useMemo(
235
+ () => ({ state, actions, hunks, headingId }),
236
+ // eslint-disable-next-line react-hooks/exhaustive-deps
237
+ [state.approved, state.totalCount, state.approvedCount, hunks, headingId],
238
+ );
239
+
240
+ return <ChangeReviewContext.Provider value={value}>{children}</ChangeReviewContext.Provider>;
241
+ }
242
+
243
+ // ─── Header ───────────────────────────────────────────────────────────────────
244
+
245
+ export type ChangeReviewHeaderProps = HTMLAttributes<HTMLDivElement>;
246
+
247
+ /**
248
+ * Count summary ("3 of 5 approved") + Approve all / Reject all bulk actions.
249
+ * `aria-controls` is intentionally omitted — screen readers reach the list
250
+ * naturally in DOM order.
251
+ */
252
+ export const ChangeReviewHeader = forwardRef<HTMLDivElement, ChangeReviewHeaderProps>(
253
+ function ChangeReviewHeader({ className, ...props }, ref) {
254
+ const { state, actions, headingId } = useChangeReview();
255
+ const { approvedCount, totalCount, allApproved, noneApproved } = state;
256
+
257
+ return (
258
+ <div
259
+ ref={ref}
260
+ className={cn("flex flex-wrap items-center justify-between gap-3 pb-3", className)}
261
+ {...props}
262
+ >
263
+ <div className="flex items-center gap-2 min-w-0">
264
+ <span id={headingId} className="text-subtitle font-semibold text-foreground truncate">
265
+ Review changes
266
+ </span>
267
+ <Badge variant={allApproved ? "success" : approvedCount > 0 ? "info" : "secondary"}>
268
+ {approvedCount} of {totalCount} approved
269
+ </Badge>
270
+ </div>
271
+
272
+ {totalCount > 0 && (
273
+ <div className="flex items-center gap-2 shrink-0">
274
+ <Button
275
+ size="sm"
276
+ variant="outline-subtle"
277
+ disabled={noneApproved}
278
+ onClick={actions.rejectAll}
279
+ aria-label="Reject all changes"
280
+ >
281
+ <X aria-hidden="true" />
282
+ Reject all
283
+ </Button>
284
+ <Button
285
+ size="sm"
286
+ variant={allApproved ? "secondary" : "default"}
287
+ disabled={allApproved}
288
+ onClick={actions.approveAll}
289
+ aria-label="Approve all changes"
290
+ >
291
+ <CheckCheck aria-hidden="true" />
292
+ Approve all
293
+ </Button>
294
+ </div>
295
+ )}
296
+ </div>
297
+ );
298
+ },
299
+ );
300
+
301
+ // ─── Provenance ───────────────────────────────────────────────────────────────
302
+
303
+ export interface ChangeReviewProvenanceProps extends HTMLAttributes<HTMLDivElement> {
304
+ /**
305
+ * Structured provenance object OR any ReactNode for custom rendering.
306
+ * When a `ChangeProvenance` object is passed, a default attribution line
307
+ * is rendered. Pass a ReactNode for full control.
308
+ */
309
+ provenance: ReactNode | ChangeProvenance;
310
+ }
311
+
312
+ function isChangeProvenance(v: unknown): v is ChangeProvenance {
313
+ return (
314
+ typeof v === "object" &&
315
+ v !== null &&
316
+ !Array.isArray(v) &&
317
+ ("author" in v || "model" in v || "timestamp" in v || "note" in v)
318
+ );
319
+ }
320
+
321
+ /**
322
+ * Quiet attribution line showing who/what proposed the change set.
323
+ * Uses `text-muted-foreground` and small typography — it should not compete
324
+ * with the hunks themselves.
325
+ */
326
+ export const ChangeReviewProvenance = forwardRef<HTMLDivElement, ChangeReviewProvenanceProps>(
327
+ function ChangeReviewProvenance({ provenance, className, ...props }, ref) {
328
+ if (isChangeProvenance(provenance)) {
329
+ const { author, model, timestamp, note } = provenance;
330
+ return (
331
+ <div
332
+ ref={ref}
333
+ className={cn(
334
+ "flex flex-wrap items-center gap-x-3 gap-y-1 text-caption text-muted-foreground pb-3",
335
+ className,
336
+ )}
337
+ aria-label="Change provenance"
338
+ {...props}
339
+ >
340
+ {author && (
341
+ <span className="flex items-center gap-1">
342
+ <User aria-hidden="true" className="size-3" />
343
+ <span>{author}</span>
344
+ </span>
345
+ )}
346
+ {model && (
347
+ <span className="flex items-center gap-1">
348
+ <Bot aria-hidden="true" className="size-3" />
349
+ <span translate="no">{model}</span>
350
+ </span>
351
+ )}
352
+ {timestamp && (
353
+ <span className="flex items-center gap-1">
354
+ <Clock aria-hidden="true" className="size-3" />
355
+ <time>{timestamp}</time>
356
+ </span>
357
+ )}
358
+ {note && <span className="text-muted-foreground/70">{note}</span>}
359
+ </div>
360
+ );
361
+ }
362
+
363
+ // Custom ReactNode provenance
364
+ return (
365
+ <div
366
+ ref={ref}
367
+ className={cn("pb-3 text-caption text-muted-foreground", className)}
368
+ aria-label="Change provenance"
369
+ {...props}
370
+ >
371
+ {provenance as ReactNode}
372
+ </div>
373
+ );
374
+ },
375
+ );
376
+
377
+ // ─── List ─────────────────────────────────────────────────────────────────────
378
+
379
+ export type ChangeReviewListProps = HTMLAttributes<HTMLOListElement>;
380
+
381
+ /**
382
+ * Ordered list of change hunks. Uses `<ol>` so AT users can navigate by
383
+ * item number ("item 2 of 5"). Renders a StatePanel when the list is empty.
384
+ */
385
+ export const ChangeReviewList = forwardRef<HTMLOListElement, ChangeReviewListProps>(
386
+ function ChangeReviewList({ className, children, ...props }, ref) {
387
+ const { hunks, headingId } = useChangeReview();
388
+
389
+ if (hunks.length === 0) {
390
+ return (
391
+ <StatePanel
392
+ kind="empty"
393
+ title="No changes to review"
394
+ description="When an agent proposes edits, they'll appear here for your approval."
395
+ className="my-2"
396
+ />
397
+ );
398
+ }
399
+
400
+ return (
401
+ <ol
402
+ ref={ref}
403
+ aria-labelledby={headingId}
404
+ className={cn("flex flex-col gap-3", className)}
405
+ {...props}
406
+ >
407
+ {children}
408
+ </ol>
409
+ );
410
+ },
411
+ );
412
+
413
+ // ─── Hunk ─────────────────────────────────────────────────────────────────────
414
+
415
+ const HUNK_STATUS_ICONS: Record<ChangeHunkStatus, ReactNode> = {
416
+ added: (
417
+ <span className="text-success-text font-bold text-meta" aria-hidden="true">
418
+ +
419
+ </span>
420
+ ),
421
+ removed: (
422
+ <span className="text-destructive font-bold text-meta" aria-hidden="true">
423
+
424
+ </span>
425
+ ),
426
+ modified: (
427
+ <span className="text-info-text font-bold text-meta" aria-hidden="true">
428
+ ~
429
+ </span>
430
+ ),
431
+ };
432
+
433
+ const HUNK_STATUS_LABELS: Record<ChangeHunkStatus, string> = {
434
+ added: "added",
435
+ removed: "removed",
436
+ modified: "modified",
437
+ };
438
+
439
+ export interface ChangeReviewHunkProps
440
+ extends
441
+ Omit<HTMLAttributes<HTMLLIElement>, "children">,
442
+ Omit<VariantProps<typeof hunkVariants>, "approved"> {
443
+ hunk: ChangeHunk;
444
+ /**
445
+ * Override the default before/after layout. Receives the hunk data and
446
+ * current approval state. Return `null` to suppress the default content area.
447
+ */
448
+ renderHunk?: (hunk: ChangeHunk, state: { approved: boolean }) => ReactNode;
449
+ }
450
+
451
+ /**
452
+ * A single change hunk row.
453
+ *
454
+ * Approval state is signalled with TWO non-color cues:
455
+ * - icon: Check (approved) vs plain toggle (pending)
456
+ * - label: "Approved" badge vs "Approve" button text
457
+ * This ensures the state is clear in all three themes including the low-chroma
458
+ * monochrome surfaces (colorblind-safe).
459
+ */
460
+ export const ChangeReviewHunk = forwardRef<HTMLLIElement, ChangeReviewHunkProps>(
461
+ function ChangeReviewHunk({ hunk, renderHunk, className, ...props }, ref) {
462
+ const { state, actions } = useChangeReview();
463
+ const { id, title, before, after, status = "modified", provenance } = hunk;
464
+ const approved = state.approved.has(id);
465
+
466
+ const statusLabel = HUNK_STATUS_LABELS[status];
467
+ const toggleLabel = approved ? `Reject hunk: ${title ?? id}` : `Approve hunk: ${title ?? id}`;
468
+
469
+ // Quiet, no-fill diff: before = struck muted text with a "−" marker, after =
470
+ // foreground text with a "+" marker — separated by space, NOT colored
471
+ // fill+border boxes. The hunk's status rail (added/removed/modified) carries
472
+ // the polarity; the markers + tone are the second, non-color cue.
473
+ const defaultContent = renderHunk
474
+ ? renderHunk(hunk, { approved })
475
+ : (before !== undefined || after !== undefined) && (
476
+ <div className="flex flex-col gap-1.5 text-body">
477
+ {before !== undefined && (
478
+ <div className="flex gap-2">
479
+ <span
480
+ aria-hidden="true"
481
+ className="shrink-0 select-none font-mono text-muted-foreground"
482
+ >
483
+
484
+ </span>
485
+ <span className="sr-only">Before: </span>
486
+ <div className="min-w-0 break-words text-muted-foreground line-through decoration-muted-foreground/30">
487
+ {before}
488
+ </div>
489
+ </div>
490
+ )}
491
+ {after !== undefined && (
492
+ <div className="flex gap-2">
493
+ <span aria-hidden="true" className="shrink-0 select-none font-mono text-foreground">
494
+ +
495
+ </span>
496
+ <span className="sr-only">After: </span>
497
+ <div className="min-w-0 break-words text-foreground">{after}</div>
498
+ </div>
499
+ )}
500
+ </div>
501
+ );
502
+
503
+ return (
504
+ <li
505
+ ref={ref}
506
+ data-hunk-id={id}
507
+ data-approved={approved}
508
+ className={cn(hunkVariants({ status }), "flex flex-col gap-3 p-4", className)}
509
+ {...props}
510
+ >
511
+ {/* Hunk header: status icon + title + per-hunk provenance + approve toggle */}
512
+ <div className="flex items-start justify-between gap-3 min-w-0">
513
+ <div className="flex items-center gap-2 min-w-0">
514
+ {/* Status indicator: NON-color cue via symbol + color together */}
515
+ <span
516
+ aria-label={`Change type: ${statusLabel}`}
517
+ className="shrink-0 inline-flex items-center justify-center size-5"
518
+ >
519
+ {HUNK_STATUS_ICONS[status]}
520
+ </span>
521
+
522
+ <span className="text-body font-medium text-foreground truncate">{title ?? id}</span>
523
+
524
+ {/* Per-hunk provenance (quiet, only when it differs from review-level) */}
525
+ {provenance?.author && (
526
+ <span className="text-caption text-muted-foreground hidden sm:inline truncate">
527
+ {provenance.author}
528
+ </span>
529
+ )}
530
+ </div>
531
+
532
+ {/* Approval toggle — must be a real button with accessible name */}
533
+ <Button
534
+ size="icon-sm"
535
+ variant={approved ? "default" : "outline-subtle"}
536
+ aria-label={toggleLabel}
537
+ aria-pressed={approved}
538
+ onClick={() => actions.toggle(id, !approved)}
539
+ className={cn(
540
+ "shrink-0 transition-colors duration-fast motion-reduce:transition-none",
541
+ approved && "bg-primary text-primary-foreground",
542
+ )}
543
+ >
544
+ {approved ? (
545
+ <Check aria-hidden="true" className="size-4" />
546
+ ) : (
547
+ <Check aria-hidden="true" className="size-4 opacity-40" />
548
+ )}
549
+ </Button>
550
+ </div>
551
+
552
+ {/* Visual approved badge — second non-color cue (icon alone is not enough) */}
553
+ {approved && (
554
+ <div className="flex items-center gap-1.5">
555
+ <Badge variant="success" className="animate-in fade-in-0 duration-fast ease-entrance">
556
+ <Check aria-hidden="true" className="size-3" />
557
+ Approved
558
+ </Badge>
559
+ </div>
560
+ )}
561
+
562
+ {/* Content area: before / after or custom renderHunk */}
563
+ {defaultContent && <div>{defaultContent}</div>}
564
+ </li>
565
+ );
566
+ },
567
+ );
568
+
569
+ // ─── Root (convenience wrapper) ───────────────────────────────────────────────
570
+
571
+ export interface ChangeReviewProps
572
+ // Omit "children" and "onToggle" from div attrs — "onToggle" is a React 19
573
+ // native ToggleEvent handler that conflicts with our review onToggle prop.
574
+ extends Omit<HTMLAttributes<HTMLDivElement>, "children" | "onToggle"> {
575
+ /** The proposed changes to review. */
576
+ hunks: ChangeHunk[];
577
+ /**
578
+ * Controlled approved set. Pass `Set<string>` or a string array.
579
+ * When omitted the component manages approval state internally (uncontrolled).
580
+ */
581
+ approved?: Set<string> | string[];
582
+ /** Called when the user toggles a single hunk's approval state. */
583
+ onToggle?: (id: string, next: boolean) => void;
584
+ /** Called when the user clicks "Approve all". */
585
+ onApproveAll?: () => void;
586
+ /** Called when the user clicks "Reject all". */
587
+ onRejectAll?: () => void;
588
+ /**
589
+ * Provenance to show above the hunk list. Accepts a `ChangeProvenance`
590
+ * object (renders default attribution) or any ReactNode.
591
+ */
592
+ provenance?: ReactNode | ChangeProvenance;
593
+ /**
594
+ * Override the per-hunk body renderer (passed through to each `ChangeReviewHunk`).
595
+ */
596
+ renderHunk?: ChangeReviewHunkProps["renderHunk"];
597
+ }
598
+
599
+ /**
600
+ * Convenience composition that wires Provider → Header → Provenance → List → Hunks.
601
+ * For custom layouts compose the parts manually via `ChangeReview.Provider`,
602
+ * `ChangeReview.Header`, etc.
603
+ */
604
+ export const ChangeReview = forwardRef<HTMLDivElement, ChangeReviewProps>(function ChangeReview(
605
+ {
606
+ hunks,
607
+ approved,
608
+ onToggle,
609
+ onApproveAll,
610
+ onRejectAll,
611
+ provenance,
612
+ renderHunk,
613
+ className,
614
+ ...props
615
+ },
616
+ ref,
617
+ ) {
618
+ return (
619
+ <ChangeReviewProvider
620
+ hunks={hunks}
621
+ approved={approved}
622
+ onToggle={onToggle}
623
+ onApproveAll={onApproveAll}
624
+ onRejectAll={onRejectAll}
625
+ >
626
+ <div ref={ref} className={cn("flex flex-col w-full", className)} {...props}>
627
+ <ChangeReviewHeader />
628
+ {provenance != null && <ChangeReviewProvenance provenance={provenance} />}
629
+ <ChangeReviewList>
630
+ {hunks.map((hunk) => (
631
+ <ChangeReviewHunk key={hunk.id} hunk={hunk} renderHunk={renderHunk} />
632
+ ))}
633
+ </ChangeReviewList>
634
+ </div>
635
+ </ChangeReviewProvider>
636
+ );
637
+ });
@@ -0,0 +1,23 @@
1
+ export {
2
+ ChangeReview,
3
+ ChangeReviewProvider,
4
+ ChangeReviewHeader,
5
+ ChangeReviewProvenance,
6
+ ChangeReviewList,
7
+ ChangeReviewHunk,
8
+ hunkVariants,
9
+ } from "./change-review";
10
+
11
+ export type {
12
+ ChangeHunk,
13
+ ChangeHunkStatus,
14
+ ChangeProvenance,
15
+ ChangeReviewState,
16
+ ChangeReviewActions,
17
+ ChangeReviewProps,
18
+ ChangeReviewProviderProps,
19
+ ChangeReviewHeaderProps,
20
+ ChangeReviewProvenanceProps,
21
+ ChangeReviewListProps,
22
+ ChangeReviewHunkProps,
23
+ } from "./change-review";