@elabs-ai/components-ui 4.0.0 → 4.2.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 (339) hide show
  1. package/README.md +10 -8
  2. package/dist/chunk-FCH4ZN6G.js +57 -0
  3. package/dist/chunk-FCH4ZN6G.js.map +1 -0
  4. package/dist/form.d.ts +28 -0
  5. package/dist/form.js +123 -0
  6. package/dist/form.js.map +1 -0
  7. package/dist/index.d.ts +2597 -188
  8. package/dist/index.js +12855 -3578
  9. package/dist/index.js.map +1 -1
  10. package/dist/lib/cn.js +10 -1
  11. package/dist/lib/cn.js.map +1 -1
  12. package/package.json +21 -7
  13. package/src/__contract__/attribution-panel.contract.test.tsx +49 -0
  14. package/src/__contract__/badge.contract.test.tsx +49 -0
  15. package/src/__contract__/bounded-number.contract.test.tsx +49 -0
  16. package/src/__contract__/button.contract.test.tsx +49 -0
  17. package/src/__contract__/color-picker.contract.test.tsx +49 -0
  18. package/src/__contract__/combobox.contract.test.tsx +49 -0
  19. package/src/__contract__/command-trigger.contract.test.tsx +49 -0
  20. package/src/__contract__/copyable-value.contract.test.tsx +49 -0
  21. package/src/__contract__/date-picker.contract.test.tsx +49 -0
  22. package/src/__contract__/date-range-picker.contract.test.tsx +49 -0
  23. package/src/__contract__/empty-state.contract.test.tsx +49 -0
  24. package/src/__contract__/error-state.contract.test.tsx +49 -0
  25. package/src/__contract__/icon-button.contract.test.tsx +49 -0
  26. package/src/__contract__/input.contract.test.tsx +49 -0
  27. package/src/__contract__/key-value-editor.contract.test.tsx +49 -0
  28. package/src/__contract__/list-editor.contract.test.tsx +49 -0
  29. package/src/__contract__/loading-state.contract.test.tsx +49 -0
  30. package/src/__contract__/metric-card.contract.test.tsx +49 -0
  31. package/src/__contract__/model-picker.contract.test.tsx +49 -0
  32. package/src/__contract__/nav-notifications.contract.test.tsx +49 -0
  33. package/src/__contract__/number-input.contract.test.tsx +49 -0
  34. package/src/__contract__/rating.contract.test.tsx +49 -0
  35. package/src/__contract__/schema-form.contract.test.tsx +49 -0
  36. package/src/__contract__/section-header.contract.test.tsx +49 -0
  37. package/src/__contract__/segmented-field.contract.test.tsx +49 -0
  38. package/src/__contract__/slider-number.contract.test.tsx +49 -0
  39. package/src/__contract__/status-badge.contract.test.tsx +49 -0
  40. package/src/__contract__/tag-input.contract.test.tsx +49 -0
  41. package/src/__contract__/text.contract.test.tsx +49 -0
  42. package/src/__contract__/textarea.contract.test.tsx +49 -0
  43. package/src/__contract__/timeline.contract.test.tsx +49 -0
  44. package/src/__contract__/toggle.contract.test.tsx +49 -0
  45. package/src/__contract__/transfer.contract.test.tsx +49 -0
  46. package/src/__contract__/tree-select.contract.test.tsx +49 -0
  47. package/src/__contract__/tree.contract.test.tsx +49 -0
  48. package/src/__contract__/virtual-select.contract.test.tsx +49 -0
  49. package/src/__contract__/workspace-picker.contract.test.tsx +49 -0
  50. package/src/_peer-deps.test.ts +59 -0
  51. package/src/blocks-comparison-table.stories.tsx +2 -2
  52. package/src/components/accordion/accordion.tsx +13 -4
  53. package/src/components/advanced-group/advanced-group.tsx +1 -1
  54. package/src/components/alert/alert.tsx +15 -4
  55. package/src/components/alert-dialog/alert-dialog.test.tsx +32 -0
  56. package/src/components/alert-dialog/alert-dialog.tsx +28 -6
  57. package/src/components/app-shell/app-shell.stories.tsx +44 -2
  58. package/src/components/app-shell/app-shell.test.tsx +99 -0
  59. package/src/components/app-shell/app-shell.tsx +56 -4
  60. package/src/components/app-sidebar/app-sidebar.stories.tsx +9 -1
  61. package/src/components/attribution-panel/attribution-panel.tsx +3 -1
  62. package/src/components/attribution-panel/attributions.generated.ts +83 -79
  63. package/src/components/avatar/avatar.tsx +4 -1
  64. package/src/components/badge/badge.stories.tsx +8 -0
  65. package/src/components/badge/badge.test.tsx +8 -0
  66. package/src/components/badge/badge.tsx +15 -5
  67. package/src/components/bento-grid/bento-grid.test.tsx +24 -3
  68. package/src/components/bento-grid/bento-grid.tsx +11 -1
  69. package/src/components/bounded-number/bounded-number.test.tsx +1 -1
  70. package/src/components/bounded-number/bounded-number.tsx +78 -75
  71. package/src/components/breadcrumb/breadcrumb.stories.tsx +38 -0
  72. package/src/components/breadcrumb/breadcrumb.test.tsx +24 -1
  73. package/src/components/breadcrumb/breadcrumb.tsx +10 -4
  74. package/src/components/button/button.stories.tsx +116 -8
  75. package/src/components/button/button.tsx +18 -12
  76. package/src/components/button-group/button-group.tsx +1 -1
  77. package/src/components/calendar/calendar.tsx +3 -2
  78. package/src/components/card/card.stories.tsx +11 -6
  79. package/src/components/card/card.test.tsx +24 -2
  80. package/src/components/card/card.tsx +48 -7
  81. package/src/components/carousel/carousel.test.tsx +169 -0
  82. package/src/components/carousel/carousel.tsx +93 -32
  83. package/src/components/change-review/change-review.stories.tsx +145 -1
  84. package/src/components/change-review/change-review.test.tsx +143 -0
  85. package/src/components/change-review/change-review.tsx +214 -24
  86. package/src/components/checkbox/checkbox.tsx +2 -1
  87. package/src/components/collapsible-panel/use-collapsible-panel.ts +0 -3
  88. package/src/components/color-picker/color-picker.tsx +22 -15
  89. package/src/components/combobox/combobox.stories.tsx +10 -0
  90. package/src/components/combobox/combobox.test.tsx +54 -0
  91. package/src/components/combobox/combobox.tsx +25 -11
  92. package/src/components/command/command.stories.tsx +16 -6
  93. package/src/components/command/command.test.tsx +88 -0
  94. package/src/components/command/command.tsx +38 -13
  95. package/src/components/command/index.ts +1 -0
  96. package/src/components/command-trigger/command-trigger.stories.tsx +39 -0
  97. package/src/components/command-trigger/command-trigger.test.tsx +18 -0
  98. package/src/components/command-trigger/command-trigger.tsx +70 -0
  99. package/src/components/command-trigger/index.ts +1 -0
  100. package/src/components/context-menu/context-menu.tsx +16 -7
  101. package/src/components/context-rail/context-rail.stories.tsx +285 -0
  102. package/src/components/context-rail/context-rail.test.tsx +263 -0
  103. package/src/components/context-rail/context-rail.tsx +678 -0
  104. package/src/components/context-rail/index.ts +1 -0
  105. package/src/components/copyable-value/copyable-value.stories.tsx +10 -0
  106. package/src/components/copyable-value/copyable-value.test.tsx +28 -1
  107. package/src/components/copyable-value/copyable-value.tsx +13 -2
  108. package/src/components/date-picker/date-picker.stories.tsx +9 -0
  109. package/src/components/date-picker/date-picker.test.tsx +52 -0
  110. package/src/components/date-picker/date-picker.tsx +13 -15
  111. package/src/components/date-range-picker/date-range-picker.stories.tsx +9 -0
  112. package/src/components/date-range-picker/date-range-picker.test.tsx +76 -1
  113. package/src/components/date-range-picker/date-range-picker.tsx +39 -28
  114. package/src/components/descriptions/descriptions.tsx +1 -1
  115. package/src/components/dialog/dialog.test.tsx +8 -3
  116. package/src/components/dialog/dialog.tsx +14 -6
  117. package/src/components/drawer/drawer.tsx +23 -6
  118. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +1 -1
  119. package/src/components/dropdown-menu/dropdown-menu.tsx +4 -4
  120. package/src/components/empty-state/empty-state.test.tsx +26 -0
  121. package/src/components/empty-state/empty-state.tsx +6 -1
  122. package/src/components/expand-dialog/expand-dialog.tsx +1 -2
  123. package/src/components/field/field-context.ts +59 -0
  124. package/src/components/field/field.stories.tsx +142 -0
  125. package/src/components/field/field.test.tsx +415 -0
  126. package/src/components/field/field.tsx +342 -0
  127. package/src/components/field/index.ts +9 -0
  128. package/src/components/field-row/field-row.stories.tsx +85 -1
  129. package/src/components/field-row/field-row.test.tsx +145 -0
  130. package/src/components/field-row/field-row.tsx +50 -6
  131. package/src/components/file-upload/file-upload.stories.tsx +16 -0
  132. package/src/components/file-upload/file-upload.test.tsx +90 -12
  133. package/src/components/file-upload/file-upload.tsx +164 -31
  134. package/src/components/form/form.tsx +8 -3
  135. package/src/components/hover-card/hover-card.tsx +1 -0
  136. package/src/components/icon-button/icon-button.test.tsx +1 -1
  137. package/src/components/icon-button/icon-button.tsx +3 -3
  138. package/src/components/input/input.stories.tsx +121 -0
  139. package/src/components/input/input.test.tsx +13 -0
  140. package/src/components/input/input.tsx +8 -4
  141. package/src/components/input-group/input-group.stories.tsx +74 -0
  142. package/src/components/input-group/input-group.tsx +18 -9
  143. package/src/components/input-otp/input-otp.tsx +8 -2
  144. package/src/components/keyboard-shortcuts/index.ts +6 -0
  145. package/src/components/keyboard-shortcuts/keyboard-shortcuts.stories.tsx +177 -0
  146. package/src/components/keyboard-shortcuts/keyboard-shortcuts.test.tsx +73 -0
  147. package/src/components/keyboard-shortcuts/keyboard-shortcuts.tsx +203 -0
  148. package/src/components/label/label.tsx +1 -1
  149. package/src/components/link-preview/link-preview.tsx +1 -1
  150. package/src/components/list-editor/list-editor.test.tsx +24 -0
  151. package/src/components/list-editor/list-editor.tsx +126 -52
  152. package/src/components/locale-provider/index.ts +6 -1
  153. package/src/components/locale-provider/locale-keys.test.ts +134 -0
  154. package/src/components/locale-provider/locale-provider.stories.tsx +89 -1
  155. package/src/components/locale-provider/locale-provider.test.tsx +257 -1
  156. package/src/components/locale-provider/locale-provider.tsx +63 -8
  157. package/src/components/locale-provider/messages.ts +822 -2
  158. package/src/components/match-highlight/match-highlight.stories.tsx +1 -1
  159. package/src/components/mention-input/mention-input-mirror.stories.tsx +9 -1
  160. package/src/components/mention-input/mention-input.stories.tsx +3 -3
  161. package/src/components/mention-input/mention-input.test.tsx +51 -1
  162. package/src/components/mention-input/mention-input.tsx +66 -46
  163. package/src/components/mention-input/mention-value.ts +8 -2
  164. package/src/components/menubar/menubar.tsx +16 -6
  165. package/src/components/metric-card/metric-card.stories.tsx +5 -3
  166. package/src/components/metric-card/metric-card.tsx +21 -2
  167. package/src/components/model-picker/model-picker.stories.tsx +63 -3
  168. package/src/components/model-picker/model-picker.test.tsx +63 -0
  169. package/src/components/model-picker/model-picker.tsx +62 -36
  170. package/src/components/nav-main/nav-main.tsx +8 -4
  171. package/src/components/nav-notifications/nav-notifications.stories.tsx +110 -0
  172. package/src/components/nav-notifications/nav-notifications.test.tsx +11 -0
  173. package/src/components/nav-notifications/nav-notifications.tsx +67 -9
  174. package/src/components/nav-user/nav-user.tsx +5 -5
  175. package/src/components/navigation-menu/navigation-menu.stories.tsx +22 -0
  176. package/src/components/navigation-menu/navigation-menu.test.tsx +174 -0
  177. package/src/components/navigation-menu/navigation-menu.tsx +204 -15
  178. package/src/components/number-input/number-input.stories.tsx +7 -3
  179. package/src/components/number-input/number-input.test.tsx +47 -0
  180. package/src/components/number-input/number-input.tsx +65 -24
  181. package/src/components/page-shell/page-shell.stories.tsx +163 -0
  182. package/src/components/page-shell/page-shell.test.tsx +229 -0
  183. package/src/components/page-shell/page-shell.tsx +108 -25
  184. package/src/components/pagination/pagination.test.tsx +109 -0
  185. package/src/components/pagination/pagination.tsx +27 -19
  186. package/src/components/popover/popover.tsx +1 -0
  187. package/src/components/progress/progress.tsx +2 -0
  188. package/src/components/radio-group/radio-group.tsx +1 -1
  189. package/src/components/rating/rating.tsx +3 -1
  190. package/src/components/resizable/resizable.stories.tsx +114 -2
  191. package/src/components/resizable/resizable.test.tsx +76 -0
  192. package/src/components/resizable/resizable.tsx +32 -6
  193. package/src/components/revision-timeline/revision-timeline.stories.tsx +8 -1
  194. package/src/components/revision-timeline/revision-timeline.test.tsx +26 -0
  195. package/src/components/revision-timeline/revision-timeline.tsx +53 -5
  196. package/src/components/schema-form/from-json-schema.test.ts +390 -0
  197. package/src/components/schema-form/from-json-schema.ts +375 -0
  198. package/src/components/schema-form/index.ts +81 -0
  199. package/src/components/schema-form/schema-form-spec.ts +698 -0
  200. package/src/components/schema-form/schema-form-store.ts +506 -0
  201. package/src/components/schema-form/schema-form.stories.tsx +566 -0
  202. package/src/components/schema-form/schema-form.test.tsx +1021 -0
  203. package/src/components/schema-form/schema-form.tsx +1381 -0
  204. package/src/components/section-header/section-header.stories.tsx +37 -0
  205. package/src/components/section-header/section-header.tsx +14 -3
  206. package/src/components/select/select.tsx +13 -7
  207. package/src/components/separator/separator.tsx +1 -0
  208. package/src/components/sheet/sheet.test.tsx +39 -0
  209. package/src/components/sheet/sheet.tsx +40 -9
  210. package/src/components/side-dock/index.ts +1 -0
  211. package/src/components/side-dock/side-dock.stories.tsx +137 -0
  212. package/src/components/side-dock/side-dock.test.tsx +299 -0
  213. package/src/components/side-dock/side-dock.tsx +515 -0
  214. package/src/components/sidebar/index.ts +1 -0
  215. package/src/components/sidebar/sidebar-frame.test.tsx +162 -0
  216. package/src/components/sidebar/sidebar.stories.tsx +421 -1
  217. package/src/components/sidebar/sidebar.test.tsx +71 -2
  218. package/src/components/sidebar/sidebar.tsx +264 -38
  219. package/src/components/skeleton/skeleton.tsx +1 -0
  220. package/src/components/skip-link/index.ts +1 -0
  221. package/src/components/skip-link/skip-link.stories.tsx +106 -0
  222. package/src/components/skip-link/skip-link.test.tsx +20 -0
  223. package/src/components/skip-link/skip-link.tsx +34 -0
  224. package/src/components/slider/slider.tsx +1 -1
  225. package/src/components/slider-number/slider-number.stories.tsx +4 -1
  226. package/src/components/slider-number/slider-number.test.tsx +1 -1
  227. package/src/components/sonner/sonner.tsx +2 -0
  228. package/src/components/spinner/spinner.test.tsx +18 -0
  229. package/src/components/spinner/spinner.tsx +14 -4
  230. package/src/components/split-panel/index.ts +6 -1
  231. package/src/components/split-panel/split-panel.stories.tsx +18 -2
  232. package/src/components/split-panel/split-panel.test.tsx +20 -0
  233. package/src/components/split-panel/split-panel.tsx +28 -8
  234. package/src/components/state-panel/state-panel.stories.tsx +52 -0
  235. package/src/components/state-panel/state-panel.test.tsx +125 -0
  236. package/src/components/state-panel/state-panel.tsx +110 -24
  237. package/src/components/status-badge/status-badge.stories.tsx +8 -0
  238. package/src/components/switch/switch.tsx +3 -1
  239. package/src/components/table/table.stories.tsx +64 -0
  240. package/src/components/table/table.test.tsx +149 -1
  241. package/src/components/table/table.tsx +71 -2
  242. package/src/components/tabs/index.ts +10 -1
  243. package/src/components/tabs/tabs.stories.tsx +48 -0
  244. package/src/components/tabs/tabs.test.tsx +67 -0
  245. package/src/components/tabs/tabs.tsx +176 -72
  246. package/src/components/tag-input/tag-input.tsx +6 -3
  247. package/src/components/team-switcher/team-switcher.tsx +35 -6
  248. package/src/components/textarea/textarea.tsx +2 -2
  249. package/src/components/theme-switcher/theme-switcher.stories.tsx +76 -0
  250. package/src/components/theme-switcher/theme-switcher.test.tsx +182 -0
  251. package/src/components/theme-switcher/theme-switcher.tsx +140 -12
  252. package/src/components/theme-switcher/use-theme-transition.ts +2 -2
  253. package/src/components/timeline/timeline.stories.tsx +27 -0
  254. package/src/components/timeline/timeline.tsx +10 -3
  255. package/src/components/toggle/toggle.tsx +7 -2
  256. package/src/components/toggle-group/toggle-group.tsx +2 -0
  257. package/src/components/toolbar/toolbar.stories.tsx +10 -2
  258. package/src/components/tooltip/tooltip.tsx +2 -1
  259. package/src/components/top-nav/top-nav.tsx +1 -1
  260. package/src/components/transfer/transfer.tsx +35 -30
  261. package/src/components/tree/tree.stories.tsx +2 -2
  262. package/src/components/tree/tree.test.tsx +41 -0
  263. package/src/components/tree/tree.tsx +91 -31
  264. package/src/components/tree/use-tree-keyboard.ts +37 -14
  265. package/src/components/tree-select/tree-select.stories.tsx +9 -0
  266. package/src/components/tree-select/tree-select.test.tsx +5 -0
  267. package/src/components/tree-select/tree-select.tsx +11 -14
  268. package/src/components/typography/prose.test.tsx +3 -2
  269. package/src/components/typography/prose.tsx +3 -3
  270. package/src/components/typography/typography.stories.tsx +228 -0
  271. package/src/components/view-toolbar/view-toolbar.stories.tsx +41 -1
  272. package/src/components/view-toolbar/view-toolbar.test.tsx +33 -0
  273. package/src/components/view-toolbar/view-toolbar.tsx +37 -4
  274. package/src/components/virtual-select/virtual-select.stories.tsx +9 -0
  275. package/src/components/virtual-select/virtual-select.test.tsx +29 -3
  276. package/src/components/virtual-select/virtual-select.tsx +56 -18
  277. package/src/components/wizard/wizard.test.tsx +9 -0
  278. package/src/components/wizard/wizard.tsx +13 -5
  279. package/src/components/workspace-picker/index.ts +3 -0
  280. package/src/components/workspace-picker/workspace-picker-state.ts +50 -0
  281. package/src/components/workspace-picker/workspace-picker.stories.tsx +126 -0
  282. package/src/components/workspace-picker/workspace-picker.test.tsx +207 -0
  283. package/src/components/workspace-picker/workspace-picker.tsx +172 -0
  284. package/src/illustrations/empty-list-illustration.tsx +26 -0
  285. package/src/illustrations/error-illustration.tsx +60 -0
  286. package/src/illustrations/first-run-illustration.tsx +27 -0
  287. package/src/illustrations/illustration-base.tsx +108 -0
  288. package/src/illustrations/illustrations.stories.tsx +94 -0
  289. package/src/illustrations/illustrations.test.tsx +98 -0
  290. package/src/illustrations/index.ts +16 -0
  291. package/src/illustrations/no-access-illustration.tsx +45 -0
  292. package/src/illustrations/no-results-illustration.tsx +46 -0
  293. package/src/illustrations/offline-illustration.tsx +37 -0
  294. package/src/illustrations/success-illustration.tsx +40 -0
  295. package/src/index.ts +80 -2
  296. package/src/lib/agent-event-model.test.ts +12 -0
  297. package/src/lib/agent-event-model.ts +42 -0
  298. package/src/lib/approval-option.test.ts +17 -0
  299. package/src/lib/approval-option.ts +53 -0
  300. package/src/lib/check-result.ts +33 -0
  301. package/src/lib/cn.ts +9 -0
  302. package/src/lib/csv.ts +48 -0
  303. package/src/lib/diff-line.test.ts +36 -0
  304. package/src/lib/diff-line.ts +63 -0
  305. package/src/lib/diff-rows.test.ts +76 -0
  306. package/src/lib/diff-rows.ts +94 -0
  307. package/src/lib/format-duration.test.ts +22 -0
  308. package/src/lib/format-duration.ts +30 -0
  309. package/src/lib/has-renderable-content.test.ts +49 -0
  310. package/src/lib/has-renderable-content.ts +32 -0
  311. package/src/lib/merge-refs.ts +2 -0
  312. package/src/lib/operating-mode.test.ts +36 -0
  313. package/src/lib/operating-mode.ts +81 -0
  314. package/src/lib/optional-peer.ts +59 -0
  315. package/src/lib/session-launch.ts +37 -0
  316. package/src/lib/slash-command.test.ts +44 -0
  317. package/src/lib/slash-command.ts +45 -0
  318. package/src/lib/trigger-query.test.ts +94 -0
  319. package/src/lib/trigger-query.ts +88 -0
  320. package/src/lib/use-controllable-state.ts +47 -0
  321. package/src/lib/use-mobile.test.tsx +47 -0
  322. package/src/lib/use-mobile.ts +34 -12
  323. package/src/motion.stories.tsx +6 -0
  324. package/src/templates-object-detail-hub.stories.tsx +2 -2
  325. package/src/templates-screen-states.stories.tsx +5 -5
  326. package/src/templates-settings.stories.tsx +1 -1
  327. package/src/blocks/sidebar-02/app-sidebar.tsx +0 -160
  328. package/src/blocks/sidebar-02/logo.tsx +0 -18
  329. package/src/blocks/sidebar-02/nav-main.tsx +0 -13
  330. package/src/blocks/sidebar-02/nav-notifications.tsx +0 -7
  331. package/src/blocks/sidebar-02/sidebar-02.stories.tsx +0 -23
  332. package/src/blocks/sidebar-02/team-switcher.tsx +0 -7
  333. package/src/blocks/sidebar-04/app-sidebar.tsx +0 -236
  334. package/src/blocks/sidebar-04/mail-context.tsx +0 -29
  335. package/src/blocks/sidebar-04/nav-user.tsx +0 -6
  336. package/src/blocks/sidebar-04/sidebar-04.stories.tsx +0 -150
  337. package/src/blocks/sidebar-05/app-sidebar.tsx +0 -375
  338. package/src/blocks/sidebar-05/sidebar-05.stories.tsx +0 -18
  339. package/src/blocks/sidebar-05/team-switcher.tsx +0 -6
@@ -65,6 +65,26 @@ describe("ChangeReview — render", () => {
65
65
  );
66
66
  expect(screen.getByTestId("custom-prov")).toBeInTheDocument();
67
67
  });
68
+
69
+ it("exposes structured provenance as a named group reachable by assistive tech", () => {
70
+ // aria-label on a role-less <div> is prohibited and silently discarded (#159) —
71
+ // assert the accessible name actually resolves via the ARIA role tree, not just
72
+ // that the attribute is present in the DOM.
73
+ render(
74
+ <ChangeReview
75
+ hunks={HUNKS}
76
+ provenance={{ author: "Atlas Agent", model: "claude-sonnet-4-6", timestamp: "14:02" }}
77
+ />,
78
+ );
79
+ expect(screen.getByRole("group", { name: "Change provenance" })).toBeInTheDocument();
80
+ });
81
+
82
+ it("exposes ReactNode provenance as a named group reachable by assistive tech", () => {
83
+ render(
84
+ <ChangeReview hunks={HUNKS} provenance={<span data-testid="custom-prov">Custom</span>} />,
85
+ );
86
+ expect(screen.getByRole("group", { name: "Change provenance" })).toBeInTheDocument();
87
+ });
68
88
  });
69
89
 
70
90
  // ─── Initial state with controlled approved set ───────────────────────────────
@@ -232,6 +252,129 @@ describe("ChangeReview — accessibility", () => {
232
252
  });
233
253
  });
234
254
 
255
+ // ─── Checks (verification evidence, #112) ─────────────────────────────────────
256
+
257
+ describe("ChangeReview — checks", () => {
258
+ it("shows a failing check's status in accessible text with an icon distinct from a passing check's", () => {
259
+ const hunksWithChecks: ChangeHunk[] = [
260
+ {
261
+ id: "h1",
262
+ title: "config/a.json",
263
+ status: "modified",
264
+ before: "old",
265
+ after: "new",
266
+ checks: [
267
+ { label: "eslint", ok: true },
268
+ { label: "tsc", ok: false, detail: "Type 'string' is not assignable to type 'number'." },
269
+ ],
270
+ },
271
+ ];
272
+ render(<ChangeReview hunks={hunksWithChecks} />);
273
+
274
+ // Accessible text carries the pass/fail status word — not color alone.
275
+ const passed = screen.getByText("Passed");
276
+ const failed = screen.getByText("Failed");
277
+ expect(passed).toBeInTheDocument();
278
+ expect(failed).toBeInTheDocument();
279
+
280
+ // The icon differs between a passing and a failing check (StatusIcon's
281
+ // canonical `complete` vs `failed` glyphs — never colour alone).
282
+ const passRow = passed.closest('[data-slot="change-review-hunk-check"]')!;
283
+ const failRow = failed.closest('[data-slot="change-review-hunk-check"]')!;
284
+ const passIcon = passRow.querySelector("svg[data-status]");
285
+ const failIcon = failRow.querySelector("svg[data-status]");
286
+ expect(passIcon).toHaveAttribute("data-status", "complete");
287
+ expect(failIcon).toHaveAttribute("data-status", "failed");
288
+ expect(passIcon?.getAttribute("data-status")).not.toBe(failIcon?.getAttribute("data-status"));
289
+ });
290
+
291
+ it("allows approving a hunk that carries a failing check", async () => {
292
+ const user = userEvent.setup();
293
+ const hunksWithFailingCheck: ChangeHunk[] = [
294
+ {
295
+ id: "h1",
296
+ title: "config/a.json",
297
+ status: "modified",
298
+ before: "old",
299
+ after: "new",
300
+ checks: [{ label: "tsc", ok: false, detail: "Type error" }],
301
+ },
302
+ ];
303
+ render(<ChangeReview hunks={hunksWithFailingCheck} />);
304
+
305
+ expect(screen.getByText("Failed")).toBeInTheDocument();
306
+
307
+ const btn = screen.getByRole("button", { name: /approve hunk: config\/a\.json/i });
308
+ await user.click(btn);
309
+
310
+ expect(screen.getByText("1 of 1 approved")).toBeInTheDocument();
311
+ expect(screen.getByText("Approved")).toBeInTheDocument();
312
+ // The failing check is still shown — informational, not blocking.
313
+ expect(screen.getByText("Failed")).toBeInTheDocument();
314
+ });
315
+
316
+ it("does not render a checks section when a hunk has no checks (strictly additive)", () => {
317
+ render(<ChangeReview hunks={HUNKS} />);
318
+ expect(screen.queryByText("Passed")).not.toBeInTheDocument();
319
+ expect(screen.queryByText("Failed")).not.toBeInTheDocument();
320
+ });
321
+
322
+ it("groups checks into Before / After sections only when both phases are present", () => {
323
+ const bothPhases: ChangeHunk[] = [
324
+ {
325
+ id: "h1",
326
+ checks: [
327
+ { label: "pre_tool_use", ok: true, phase: "before" },
328
+ { label: "post_tool_use", ok: true, phase: "after" },
329
+ ],
330
+ },
331
+ ];
332
+ render(<ChangeReview hunks={bothPhases} />);
333
+ expect(screen.getByText("Before")).toBeInTheDocument();
334
+ expect(screen.getByText("After")).toBeInTheDocument();
335
+ });
336
+
337
+ it("does not render phase headers when only one phase is present", () => {
338
+ const onePhase: ChangeHunk[] = [
339
+ {
340
+ id: "h1",
341
+ checks: [{ label: "eslint", ok: true, phase: "before" }],
342
+ },
343
+ ];
344
+ render(<ChangeReview hunks={onePhase} />);
345
+ expect(screen.queryByText("Before")).not.toBeInTheDocument();
346
+ expect(screen.queryByText("After")).not.toBeInTheDocument();
347
+ expect(screen.getByText("eslint")).toBeInTheDocument();
348
+ });
349
+
350
+ it("renders a short detail line inline and shows the duration", () => {
351
+ const hunksWithDuration: ChangeHunk[] = [
352
+ { id: "h1", checks: [{ label: "eslint", ok: true, detail: "0 problems", durationMs: 128 }] },
353
+ ];
354
+ render(<ChangeReview hunks={hunksWithDuration} />);
355
+ expect(screen.getByText("0 problems")).toBeInTheDocument();
356
+ expect(screen.getByText("128ms")).toBeInTheDocument();
357
+ });
358
+
359
+ it("collapses a long detail line behind a disclosure, expandable on click", async () => {
360
+ const user = userEvent.setup();
361
+ const longDetail =
362
+ "This is a deliberately long detail line that exceeds the inline collapse " +
363
+ "threshold and should therefore render behind a disclosure toggle by default.";
364
+ const hunksWithLongDetail: ChangeHunk[] = [
365
+ { id: "h1", checks: [{ label: "tsc", ok: false, detail: longDetail }] },
366
+ ];
367
+ render(<ChangeReview hunks={hunksWithLongDetail} />);
368
+
369
+ // Collapsed by default: the detail text is not yet in the document.
370
+ expect(screen.queryByText(longDetail)).not.toBeInTheDocument();
371
+ const trigger = screen.getByRole("button", { name: /show detail/i });
372
+
373
+ await user.click(trigger);
374
+ expect(await screen.findByText(longDetail)).toBeInTheDocument();
375
+ });
376
+ });
377
+
235
378
  // ─── Compound usage ───────────────────────────────────────────────────────────
236
379
 
237
380
  describe("ChangeReview — compound component parts", () => {
@@ -11,6 +11,27 @@
11
11
  * Design references: GitHub PR file-review (per-hunk viewed/comment),
12
12
  * Cursor/Copilot "review changes" panel, Graphite/Linear review UIs.
13
13
  *
14
+ * NOT a diff-line renderer, and deliberately never becoming one. A hunk's
15
+ * `before`/`after` are ordinary `ReactNode` prose, drawn as a quiet no-fill
16
+ * comparison; the `−`/`+` markers below mean "previous content"/"proposed
17
+ * content", which is not the same idea as `DiffLine`'s "removed line"/"added
18
+ * line" even though the glyphs coincide. Do NOT reach for `diffLineMarker` here:
19
+ * a coincidence stays a literal (theming.md, intentional-mirror rule).
20
+ *
21
+ * Structured `DiffLine[]` hunks reach this component by INJECTION, which is a
22
+ * binding decision, not an unfinished merge — see
23
+ * `docs/decisions/2026-09-01-brainless-adoption-architecture.md` § 3:
24
+ * - `ChangeHunk` gains NO `lines` field and `DiffLine` never moves here.
25
+ * - The app that depends on both packages passes a `<DiffView>`
26
+ * (`@elabs-ai/components-ai`, Shiki-backed) through `renderHunk` or a hunk's
27
+ * `after` slot. Neither package imports the other, in either direction.
28
+ * - Re-litigated and re-ratified 2026-09-03: moving this component into
29
+ * `@elabs-ai/components-ai` was rejected because it would push a
30
+ * dependency-free component into a layer-2 leaf that `terminal`, `editor`
31
+ * and `viewer` could then never reach.
32
+ * See the `InjectedHunkRenderer` story here, and `ChangeReviewComposition` in
33
+ * `packages/ai/src/diff-view.stories.tsx` for the real two-package wiring.
34
+ *
14
35
  * State grid: empty (no hunks → StatePanel), single hunk, mixed, all-approved,
15
36
  * all-rejected, very long hunk content. Approval state is signalled with color
16
37
  * AND icon/label (colorblind + high-decoration/high-contrast safe).
@@ -36,12 +57,17 @@ import {
36
57
  type HTMLAttributes,
37
58
  type ReactNode,
38
59
  } from "react";
39
- import { Check, X, CheckCheck, User, Bot, Clock } from "lucide-react";
60
+ import { Check, X, CheckCheck, User, Bot, Clock, ChevronDown } from "lucide-react";
40
61
  import { cva, type VariantProps } from "class-variance-authority";
41
62
  import { cn } from "../../lib/cn";
63
+ import { type CheckResult } from "../../lib/check-result";
64
+ import { formatElapsed } from "../../lib/format-duration";
42
65
  import { Button } from "../button/button";
43
66
  import { Badge } from "../badge/badge";
44
67
  import { StatePanel } from "../state-panel/state-panel";
68
+ import { StatusIcon } from "../status-badge/status-badge";
69
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../collapsible/collapsible";
70
+ import { useLocale } from "../locale-provider";
45
71
 
46
72
  // ─── Public types ─────────────────────────────────────────────────────────────
47
73
 
@@ -74,6 +100,13 @@ export interface ChangeHunk {
74
100
  status?: ChangeHunkStatus;
75
101
  /** Per-hunk provenance override (overrides the review-level provenance). */
76
102
  provenance?: ChangeProvenance;
103
+ /**
104
+ * Verification results for this hunk (lint / types / tests / policy hooks).
105
+ * Purely informational — a failing check never blocks approval, it only
106
+ * informs the reviewer's decision. Grouped into "Before" / "After" sections
107
+ * when both phases are present.
108
+ */
109
+ checks?: CheckResult[];
77
110
  }
78
111
 
79
112
  /** State surface exposed by the ChangeReview context. */
@@ -251,6 +284,7 @@ export type ChangeReviewHeaderProps = HTMLAttributes<HTMLDivElement>;
251
284
  */
252
285
  export const ChangeReviewHeader = forwardRef<HTMLDivElement, ChangeReviewHeaderProps>(
253
286
  function ChangeReviewHeader({ className, ...props }, ref) {
287
+ const { t } = useLocale();
254
288
  const { state, actions, headingId } = useChangeReview();
255
289
  const { approvedCount, totalCount, allApproved, noneApproved } = state;
256
290
 
@@ -262,10 +296,10 @@ export const ChangeReviewHeader = forwardRef<HTMLDivElement, ChangeReviewHeaderP
262
296
  >
263
297
  <div className="flex items-center gap-2 min-w-0">
264
298
  <span id={headingId} className="text-subtitle font-semibold text-foreground truncate">
265
- Review changes
299
+ {t("ui.changeReview.heading")}
266
300
  </span>
267
301
  <Badge variant={allApproved ? "success" : approvedCount > 0 ? "info" : "secondary"}>
268
- {approvedCount} of {totalCount} approved
302
+ {t("ui.changeReview.approvedCount", { approved: approvedCount, total: totalCount })}
269
303
  </Badge>
270
304
  </div>
271
305
 
@@ -276,20 +310,20 @@ export const ChangeReviewHeader = forwardRef<HTMLDivElement, ChangeReviewHeaderP
276
310
  variant="outline-subtle"
277
311
  disabled={noneApproved}
278
312
  onClick={actions.rejectAll}
279
- aria-label="Reject all changes"
313
+ aria-label={t("ui.changeReview.rejectAllLabel")}
280
314
  >
281
315
  <X aria-hidden="true" />
282
- Reject all
316
+ {t("ui.changeReview.rejectAll")}
283
317
  </Button>
284
318
  <Button
285
319
  size="sm"
286
320
  variant={allApproved ? "secondary" : "default"}
287
321
  disabled={allApproved}
288
322
  onClick={actions.approveAll}
289
- aria-label="Approve all changes"
323
+ aria-label={t("ui.changeReview.approveAllLabel")}
290
324
  >
291
325
  <CheckCheck aria-hidden="true" />
292
- Approve all
326
+ {t("ui.changeReview.approveAll")}
293
327
  </Button>
294
328
  </div>
295
329
  )}
@@ -325,6 +359,7 @@ function isChangeProvenance(v: unknown): v is ChangeProvenance {
325
359
  */
326
360
  export const ChangeReviewProvenance = forwardRef<HTMLDivElement, ChangeReviewProvenanceProps>(
327
361
  function ChangeReviewProvenance({ provenance, className, ...props }, ref) {
362
+ const { t } = useLocale();
328
363
  if (isChangeProvenance(provenance)) {
329
364
  const { author, model, timestamp, note } = provenance;
330
365
  return (
@@ -334,7 +369,8 @@ export const ChangeReviewProvenance = forwardRef<HTMLDivElement, ChangeReviewPro
334
369
  "flex flex-wrap items-center gap-x-3 gap-y-1 text-caption text-muted-foreground pb-3",
335
370
  className,
336
371
  )}
337
- aria-label="Change provenance"
372
+ role="group"
373
+ aria-label={t("ui.changeReview.provenanceLabel")}
338
374
  {...props}
339
375
  >
340
376
  {author && (
@@ -365,7 +401,8 @@ export const ChangeReviewProvenance = forwardRef<HTMLDivElement, ChangeReviewPro
365
401
  <div
366
402
  ref={ref}
367
403
  className={cn("pb-3 text-caption text-muted-foreground", className)}
368
- aria-label="Change provenance"
404
+ role="group"
405
+ aria-label={t("ui.changeReview.provenanceLabel")}
369
406
  {...props}
370
407
  >
371
408
  {provenance as ReactNode}
@@ -384,14 +421,15 @@ export type ChangeReviewListProps = HTMLAttributes<HTMLOListElement>;
384
421
  */
385
422
  export const ChangeReviewList = forwardRef<HTMLOListElement, ChangeReviewListProps>(
386
423
  function ChangeReviewList({ className, children, ...props }, ref) {
424
+ const { t } = useLocale();
387
425
  const { hunks, headingId } = useChangeReview();
388
426
 
389
427
  if (hunks.length === 0) {
390
428
  return (
391
429
  <StatePanel
392
430
  kind="empty"
393
- title="No changes to review"
394
- description="When an agent proposes edits, they'll appear here for your approval."
431
+ title={t("ui.changeReview.emptyTitle")}
432
+ description={t("ui.changeReview.emptyDescription")}
395
433
  className="my-2"
396
434
  />
397
435
  );
@@ -419,7 +457,11 @@ const HUNK_STATUS_ICONS: Record<ChangeHunkStatus, ReactNode> = {
419
457
  </span>
420
458
  ),
421
459
  removed: (
422
- <span className="text-destructive font-bold text-meta" aria-hidden="true">
460
+ // #124: sibling of `added`/`modified` above — this glyph is a decorative
461
+ // aria-hidden marker beside a labelled span (see HUNK_STATUS_LABELS), and
462
+ // reads as running text at the type scale, so it takes the ink rung the
463
+ // other two already use.
464
+ <span className="text-destructive-text font-bold text-meta" aria-hidden="true">
423
465
 
424
466
  </span>
425
467
  ),
@@ -430,12 +472,153 @@ const HUNK_STATUS_ICONS: Record<ChangeHunkStatus, ReactNode> = {
430
472
  ),
431
473
  };
432
474
 
433
- const HUNK_STATUS_LABELS: Record<ChangeHunkStatus, string> = {
434
- added: "added",
435
- removed: "removed",
436
- modified: "modified",
475
+ const HUNK_STATUS_LABEL_KEYS: Record<ChangeHunkStatus, string> = {
476
+ added: "ui.changeReview.statusAdded",
477
+ removed: "ui.changeReview.statusRemoved",
478
+ modified: "ui.changeReview.statusModified",
479
+ };
480
+
481
+ // ─── Checks (verification evidence) ────────────────────────────────────────────
482
+
483
+ /**
484
+ * A check's `detail` collapses behind a disclosure once it's long enough that
485
+ * showing it inline would dominate the hunk row. Short details always render
486
+ * in place.
487
+ */
488
+ const CHECK_DETAIL_COLLAPSE_THRESHOLD = 80;
489
+
490
+ function isLongCheckDetail(detail: string): boolean {
491
+ return detail.length > CHECK_DETAIL_COLLAPSE_THRESHOLD || detail.includes("\n");
492
+ }
493
+
494
+ const CHECK_PHASE_LABEL_KEYS: Record<"before" | "after", string> = {
495
+ before: "ui.changeReview.phaseBefore",
496
+ after: "ui.changeReview.phaseAfter",
437
497
  };
438
498
 
499
+ /**
500
+ * Group checks for display. Phase headers ("Before" / "After") appear ONLY
501
+ * when both phases are present in the same hunk — a single-phase or
502
+ * unphased list of checks renders flat, with no header noise.
503
+ */
504
+ function groupChecksByPhase(
505
+ checks: CheckResult[],
506
+ ): { phase?: "before" | "after"; items: CheckResult[] }[] {
507
+ const before = checks.filter((c) => c.phase === "before");
508
+ const after = checks.filter((c) => c.phase === "after");
509
+ const showPhaseLabels = before.length > 0 && after.length > 0;
510
+
511
+ if (!showPhaseLabels) {
512
+ return [{ items: checks }];
513
+ }
514
+
515
+ const rest = checks.filter((c) => c.phase !== "before" && c.phase !== "after");
516
+ const groups: { phase?: "before" | "after"; items: CheckResult[] }[] = [];
517
+ if (rest.length > 0) groups.push({ items: rest });
518
+ groups.push({ phase: "before", items: before });
519
+ groups.push({ phase: "after", items: after });
520
+ return groups;
521
+ }
522
+
523
+ /**
524
+ * A single verification result row: icon + label + accessible status word +
525
+ * optional duration, with an optional collapsed detail line below.
526
+ *
527
+ * The pass/fail signal is carried by TWO non-color cues — the icon (distinct
528
+ * glyph per `StatusIcon`'s canonical vocabulary) and the visible "Passed"/
529
+ * "Failed" text — so it survives in greyscale (WCAG 1.4.1). `StatusIcon` is
530
+ * decorative (`aria-hidden`); the status word is what reaches assistive tech.
531
+ */
532
+ function ChangeCheckRow({ check }: { check: CheckResult }) {
533
+ const { t } = useLocale();
534
+ const [detailOpen, setDetailOpen] = useState(false);
535
+ const { label, ok, detail, durationMs } = check;
536
+ const statusWord = ok ? t("ui.changeReview.checkPassed") : t("ui.changeReview.checkFailed");
537
+ const hasDetail = detail !== undefined && detail !== "";
538
+ const longDetail = hasDetail && isLongCheckDetail(detail);
539
+
540
+ return (
541
+ <div data-slot="change-review-hunk-check" data-ok={ok} className="flex flex-col gap-1">
542
+ <div className="flex flex-wrap items-center gap-x-1.5 gap-y-0.5 min-w-0">
543
+ <StatusIcon status={ok ? "complete" : "failed"} className="size-3.5 shrink-0" />
544
+ <span className="text-caption font-medium text-foreground truncate">{label}</span>
545
+ <span className={cn("text-caption", ok ? "text-success-text" : "text-destructive-text")}>
546
+ {statusWord}
547
+ </span>
548
+ {durationMs !== undefined && (
549
+ <span className="text-caption text-muted-foreground tabular-nums shrink-0">
550
+ {formatElapsed(durationMs)}
551
+ </span>
552
+ )}
553
+ </div>
554
+
555
+ {hasDetail &&
556
+ (longDetail ? (
557
+ <Collapsible open={detailOpen} onOpenChange={setDetailOpen}>
558
+ <CollapsibleTrigger
559
+ className={cn(
560
+ "flex items-center gap-1 self-start rounded-sm ps-5 text-caption text-muted-foreground",
561
+ "hover:text-foreground focus-ring",
562
+ )}
563
+ >
564
+ <ChevronDown
565
+ aria-hidden="true"
566
+ className={cn(
567
+ "size-3 transition-transform duration-fast motion-reduce:transition-none",
568
+ detailOpen ? "rotate-180" : "rotate-0",
569
+ )}
570
+ />
571
+ {detailOpen ? t("ui.changeReview.hideDetail") : t("ui.changeReview.showDetail")}
572
+ </CollapsibleTrigger>
573
+ <CollapsibleContent data-slot="change-review-hunk-check-detail">
574
+ <p className="ps-5 pt-1 text-caption text-muted-foreground break-words">{detail}</p>
575
+ </CollapsibleContent>
576
+ </Collapsible>
577
+ ) : (
578
+ <p
579
+ data-slot="change-review-hunk-check-detail"
580
+ className="ps-5 text-caption text-muted-foreground break-words"
581
+ >
582
+ {detail}
583
+ </p>
584
+ ))}
585
+ </div>
586
+ );
587
+ }
588
+
589
+ /** The full checks section for one hunk — grouped by phase when applicable. */
590
+ function ChangeReviewChecks({ checks }: { checks: CheckResult[] }) {
591
+ const { t } = useLocale();
592
+ const groups = groupChecksByPhase(checks);
593
+ const showPhaseLabels = groups.some((g) => g.phase !== undefined);
594
+
595
+ return (
596
+ <div
597
+ data-slot="change-review-hunk-checks"
598
+ className="flex flex-col gap-2 border-t border-border pt-3"
599
+ >
600
+ {groups.map((group, index) => (
601
+ <div
602
+ key={group.phase ?? `_${index}`}
603
+ data-slot="change-review-hunk-checks-phase"
604
+ className="flex flex-col gap-1.5"
605
+ >
606
+ {showPhaseLabels && group.phase && (
607
+ <span className="text-meta font-medium text-muted-foreground">
608
+ {t(CHECK_PHASE_LABEL_KEYS[group.phase])}
609
+ </span>
610
+ )}
611
+ <div className="flex flex-col gap-1.5">
612
+ {group.items.map((check, itemIndex) => (
613
+ <ChangeCheckRow key={`${check.label}-${itemIndex}`} check={check} />
614
+ ))}
615
+ </div>
616
+ </div>
617
+ ))}
618
+ </div>
619
+ );
620
+ }
621
+
439
622
  export interface ChangeReviewHunkProps
440
623
  extends
441
624
  Omit<HTMLAttributes<HTMLLIElement>, "children">,
@@ -454,17 +637,20 @@ export interface ChangeReviewHunkProps
454
637
  * Approval state is signalled with TWO non-color cues:
455
638
  * - icon: Check (approved) vs plain toggle (pending)
456
639
  * - label: "Approved" badge vs "Approve" button text
457
- * This ensures the state is clear in all three themes including the low-chroma
640
+ * This ensures the state is clear in every theme including the low-chroma
458
641
  * monochrome surfaces (colorblind-safe).
459
642
  */
460
643
  export const ChangeReviewHunk = forwardRef<HTMLLIElement, ChangeReviewHunkProps>(
461
644
  function ChangeReviewHunk({ hunk, renderHunk, className, ...props }, ref) {
645
+ const { t } = useLocale();
462
646
  const { state, actions } = useChangeReview();
463
- const { id, title, before, after, status = "modified", provenance } = hunk;
647
+ const { id, title, before, after, status = "modified", provenance, checks } = hunk;
464
648
  const approved = state.approved.has(id);
465
649
 
466
- const statusLabel = HUNK_STATUS_LABELS[status];
467
- const toggleLabel = approved ? `Reject hunk: ${title ?? id}` : `Approve hunk: ${title ?? id}`;
650
+ const statusLabel = t(HUNK_STATUS_LABEL_KEYS[status]);
651
+ const toggleLabel = t(approved ? "ui.changeReview.rejectHunk" : "ui.changeReview.approveHunk", {
652
+ title: title ?? id,
653
+ });
468
654
 
469
655
  // Quiet, no-fill diff: before = struck muted text with a "−" marker, after =
470
656
  // foreground text with a "+" marker — separated by space, NOT colored
@@ -482,7 +668,7 @@ export const ChangeReviewHunk = forwardRef<HTMLLIElement, ChangeReviewHunkProps>
482
668
  >
483
669
 
484
670
  </span>
485
- <span className="sr-only">Before: </span>
671
+ <span className="sr-only">{t("ui.changeReview.beforePrefix")}</span>
486
672
  <div className="min-w-0 break-words text-muted-foreground line-through decoration-muted-foreground/30">
487
673
  {before}
488
674
  </div>
@@ -493,7 +679,7 @@ export const ChangeReviewHunk = forwardRef<HTMLLIElement, ChangeReviewHunkProps>
493
679
  <span aria-hidden="true" className="shrink-0 select-none font-mono text-foreground">
494
680
  +
495
681
  </span>
496
- <span className="sr-only">After: </span>
682
+ <span className="sr-only">{t("ui.changeReview.afterPrefix")}</span>
497
683
  <div className="min-w-0 break-words text-foreground">{after}</div>
498
684
  </div>
499
685
  )}
@@ -513,7 +699,8 @@ export const ChangeReviewHunk = forwardRef<HTMLLIElement, ChangeReviewHunkProps>
513
699
  <div className="flex items-center gap-2 min-w-0">
514
700
  {/* Status indicator: NON-color cue via symbol + color together */}
515
701
  <span
516
- aria-label={`Change type: ${statusLabel}`}
702
+ role="img"
703
+ aria-label={t("ui.changeReview.changeType", { status: statusLabel })}
517
704
  className="shrink-0 inline-flex items-center justify-center size-5"
518
705
  >
519
706
  {HUNK_STATUS_ICONS[status]}
@@ -554,13 +741,16 @@ export const ChangeReviewHunk = forwardRef<HTMLLIElement, ChangeReviewHunkProps>
554
741
  <div className="flex items-center gap-1.5">
555
742
  <Badge variant="success" className="animate-in fade-in-0 duration-fast ease-entrance">
556
743
  <Check aria-hidden="true" className="size-3" />
557
- Approved
744
+ {t("ui.changeReview.approvedBadge")}
558
745
  </Badge>
559
746
  </div>
560
747
  )}
561
748
 
562
749
  {/* Content area: before / after or custom renderHunk */}
563
750
  {defaultContent && <div>{defaultContent}</div>}
751
+
752
+ {/* Verification evidence — informational only, never blocks approval */}
753
+ {checks && checks.length > 0 && <ChangeReviewChecks checks={checks} />}
564
754
  </li>
565
755
  );
566
756
  },
@@ -10,9 +10,10 @@ export const Checkbox = forwardRef<
10
10
  return (
11
11
  <CheckboxPrimitive.Root
12
12
  ref={ref}
13
+ data-slot="checkbox"
13
14
  className={cn(
14
15
  "peer size-4 shrink-0 rounded-sm border border-input shadow-sm transition-colors duration-fast ease-standard",
15
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
16
+ "focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
16
17
  "disabled:cursor-not-allowed disabled:opacity-50",
17
18
  "data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
18
19
  className,
@@ -25,9 +25,6 @@
25
25
  * state-keyed defaults for a generic panel. Sidebar passes its
26
26
  * `--sidebar-width` + `group-data-[collapsible=offcanvas]` literals and gets
27
27
  * back its exact original fragments (the byte-identical re-point).
28
- *
29
- * Fork-prevention: a second gap-spacer + fixed-slide width tween outside this
30
- * folder fails `pnpm collapse-fork:check` (scripts/check-collapse-fork.mjs).
31
28
  */
32
29
  import { useCallback, useMemo, useState } from "react";
33
30
  import { cn } from "../../lib/cn";