@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.
- package/README.md +10 -8
- package/dist/chunk-FCH4ZN6G.js +57 -0
- package/dist/chunk-FCH4ZN6G.js.map +1 -0
- package/dist/form.d.ts +28 -0
- package/dist/form.js +123 -0
- package/dist/form.js.map +1 -0
- package/dist/index.d.ts +2597 -188
- package/dist/index.js +12855 -3578
- package/dist/index.js.map +1 -1
- package/dist/lib/cn.js +10 -1
- package/dist/lib/cn.js.map +1 -1
- package/package.json +21 -7
- package/src/__contract__/attribution-panel.contract.test.tsx +49 -0
- package/src/__contract__/badge.contract.test.tsx +49 -0
- package/src/__contract__/bounded-number.contract.test.tsx +49 -0
- package/src/__contract__/button.contract.test.tsx +49 -0
- package/src/__contract__/color-picker.contract.test.tsx +49 -0
- package/src/__contract__/combobox.contract.test.tsx +49 -0
- package/src/__contract__/command-trigger.contract.test.tsx +49 -0
- package/src/__contract__/copyable-value.contract.test.tsx +49 -0
- package/src/__contract__/date-picker.contract.test.tsx +49 -0
- package/src/__contract__/date-range-picker.contract.test.tsx +49 -0
- package/src/__contract__/empty-state.contract.test.tsx +49 -0
- package/src/__contract__/error-state.contract.test.tsx +49 -0
- package/src/__contract__/icon-button.contract.test.tsx +49 -0
- package/src/__contract__/input.contract.test.tsx +49 -0
- package/src/__contract__/key-value-editor.contract.test.tsx +49 -0
- package/src/__contract__/list-editor.contract.test.tsx +49 -0
- package/src/__contract__/loading-state.contract.test.tsx +49 -0
- package/src/__contract__/metric-card.contract.test.tsx +49 -0
- package/src/__contract__/model-picker.contract.test.tsx +49 -0
- package/src/__contract__/nav-notifications.contract.test.tsx +49 -0
- package/src/__contract__/number-input.contract.test.tsx +49 -0
- package/src/__contract__/rating.contract.test.tsx +49 -0
- package/src/__contract__/schema-form.contract.test.tsx +49 -0
- package/src/__contract__/section-header.contract.test.tsx +49 -0
- package/src/__contract__/segmented-field.contract.test.tsx +49 -0
- package/src/__contract__/slider-number.contract.test.tsx +49 -0
- package/src/__contract__/status-badge.contract.test.tsx +49 -0
- package/src/__contract__/tag-input.contract.test.tsx +49 -0
- package/src/__contract__/text.contract.test.tsx +49 -0
- package/src/__contract__/textarea.contract.test.tsx +49 -0
- package/src/__contract__/timeline.contract.test.tsx +49 -0
- package/src/__contract__/toggle.contract.test.tsx +49 -0
- package/src/__contract__/transfer.contract.test.tsx +49 -0
- package/src/__contract__/tree-select.contract.test.tsx +49 -0
- package/src/__contract__/tree.contract.test.tsx +49 -0
- package/src/__contract__/virtual-select.contract.test.tsx +49 -0
- package/src/__contract__/workspace-picker.contract.test.tsx +49 -0
- package/src/_peer-deps.test.ts +59 -0
- package/src/blocks-comparison-table.stories.tsx +2 -2
- package/src/components/accordion/accordion.tsx +13 -4
- package/src/components/advanced-group/advanced-group.tsx +1 -1
- package/src/components/alert/alert.tsx +15 -4
- package/src/components/alert-dialog/alert-dialog.test.tsx +32 -0
- package/src/components/alert-dialog/alert-dialog.tsx +28 -6
- package/src/components/app-shell/app-shell.stories.tsx +44 -2
- package/src/components/app-shell/app-shell.test.tsx +99 -0
- package/src/components/app-shell/app-shell.tsx +56 -4
- package/src/components/app-sidebar/app-sidebar.stories.tsx +9 -1
- package/src/components/attribution-panel/attribution-panel.tsx +3 -1
- package/src/components/attribution-panel/attributions.generated.ts +83 -79
- package/src/components/avatar/avatar.tsx +4 -1
- package/src/components/badge/badge.stories.tsx +8 -0
- package/src/components/badge/badge.test.tsx +8 -0
- package/src/components/badge/badge.tsx +15 -5
- package/src/components/bento-grid/bento-grid.test.tsx +24 -3
- package/src/components/bento-grid/bento-grid.tsx +11 -1
- package/src/components/bounded-number/bounded-number.test.tsx +1 -1
- package/src/components/bounded-number/bounded-number.tsx +78 -75
- package/src/components/breadcrumb/breadcrumb.stories.tsx +38 -0
- package/src/components/breadcrumb/breadcrumb.test.tsx +24 -1
- package/src/components/breadcrumb/breadcrumb.tsx +10 -4
- package/src/components/button/button.stories.tsx +116 -8
- package/src/components/button/button.tsx +18 -12
- package/src/components/button-group/button-group.tsx +1 -1
- package/src/components/calendar/calendar.tsx +3 -2
- package/src/components/card/card.stories.tsx +11 -6
- package/src/components/card/card.test.tsx +24 -2
- package/src/components/card/card.tsx +48 -7
- package/src/components/carousel/carousel.test.tsx +169 -0
- package/src/components/carousel/carousel.tsx +93 -32
- package/src/components/change-review/change-review.stories.tsx +145 -1
- package/src/components/change-review/change-review.test.tsx +143 -0
- package/src/components/change-review/change-review.tsx +214 -24
- package/src/components/checkbox/checkbox.tsx +2 -1
- package/src/components/collapsible-panel/use-collapsible-panel.ts +0 -3
- package/src/components/color-picker/color-picker.tsx +22 -15
- package/src/components/combobox/combobox.stories.tsx +10 -0
- package/src/components/combobox/combobox.test.tsx +54 -0
- package/src/components/combobox/combobox.tsx +25 -11
- package/src/components/command/command.stories.tsx +16 -6
- package/src/components/command/command.test.tsx +88 -0
- package/src/components/command/command.tsx +38 -13
- package/src/components/command/index.ts +1 -0
- package/src/components/command-trigger/command-trigger.stories.tsx +39 -0
- package/src/components/command-trigger/command-trigger.test.tsx +18 -0
- package/src/components/command-trigger/command-trigger.tsx +70 -0
- package/src/components/command-trigger/index.ts +1 -0
- package/src/components/context-menu/context-menu.tsx +16 -7
- package/src/components/context-rail/context-rail.stories.tsx +285 -0
- package/src/components/context-rail/context-rail.test.tsx +263 -0
- package/src/components/context-rail/context-rail.tsx +678 -0
- package/src/components/context-rail/index.ts +1 -0
- package/src/components/copyable-value/copyable-value.stories.tsx +10 -0
- package/src/components/copyable-value/copyable-value.test.tsx +28 -1
- package/src/components/copyable-value/copyable-value.tsx +13 -2
- package/src/components/date-picker/date-picker.stories.tsx +9 -0
- package/src/components/date-picker/date-picker.test.tsx +52 -0
- package/src/components/date-picker/date-picker.tsx +13 -15
- package/src/components/date-range-picker/date-range-picker.stories.tsx +9 -0
- package/src/components/date-range-picker/date-range-picker.test.tsx +76 -1
- package/src/components/date-range-picker/date-range-picker.tsx +39 -28
- package/src/components/descriptions/descriptions.tsx +1 -1
- package/src/components/dialog/dialog.test.tsx +8 -3
- package/src/components/dialog/dialog.tsx +14 -6
- package/src/components/drawer/drawer.tsx +23 -6
- package/src/components/dropdown-menu/dropdown-menu.stories.tsx +1 -1
- package/src/components/dropdown-menu/dropdown-menu.tsx +4 -4
- package/src/components/empty-state/empty-state.test.tsx +26 -0
- package/src/components/empty-state/empty-state.tsx +6 -1
- package/src/components/expand-dialog/expand-dialog.tsx +1 -2
- package/src/components/field/field-context.ts +59 -0
- package/src/components/field/field.stories.tsx +142 -0
- package/src/components/field/field.test.tsx +415 -0
- package/src/components/field/field.tsx +342 -0
- package/src/components/field/index.ts +9 -0
- package/src/components/field-row/field-row.stories.tsx +85 -1
- package/src/components/field-row/field-row.test.tsx +145 -0
- package/src/components/field-row/field-row.tsx +50 -6
- package/src/components/file-upload/file-upload.stories.tsx +16 -0
- package/src/components/file-upload/file-upload.test.tsx +90 -12
- package/src/components/file-upload/file-upload.tsx +164 -31
- package/src/components/form/form.tsx +8 -3
- package/src/components/hover-card/hover-card.tsx +1 -0
- package/src/components/icon-button/icon-button.test.tsx +1 -1
- package/src/components/icon-button/icon-button.tsx +3 -3
- package/src/components/input/input.stories.tsx +121 -0
- package/src/components/input/input.test.tsx +13 -0
- package/src/components/input/input.tsx +8 -4
- package/src/components/input-group/input-group.stories.tsx +74 -0
- package/src/components/input-group/input-group.tsx +18 -9
- package/src/components/input-otp/input-otp.tsx +8 -2
- package/src/components/keyboard-shortcuts/index.ts +6 -0
- package/src/components/keyboard-shortcuts/keyboard-shortcuts.stories.tsx +177 -0
- package/src/components/keyboard-shortcuts/keyboard-shortcuts.test.tsx +73 -0
- package/src/components/keyboard-shortcuts/keyboard-shortcuts.tsx +203 -0
- package/src/components/label/label.tsx +1 -1
- package/src/components/link-preview/link-preview.tsx +1 -1
- package/src/components/list-editor/list-editor.test.tsx +24 -0
- package/src/components/list-editor/list-editor.tsx +126 -52
- package/src/components/locale-provider/index.ts +6 -1
- package/src/components/locale-provider/locale-keys.test.ts +134 -0
- package/src/components/locale-provider/locale-provider.stories.tsx +89 -1
- package/src/components/locale-provider/locale-provider.test.tsx +257 -1
- package/src/components/locale-provider/locale-provider.tsx +63 -8
- package/src/components/locale-provider/messages.ts +822 -2
- package/src/components/match-highlight/match-highlight.stories.tsx +1 -1
- package/src/components/mention-input/mention-input-mirror.stories.tsx +9 -1
- package/src/components/mention-input/mention-input.stories.tsx +3 -3
- package/src/components/mention-input/mention-input.test.tsx +51 -1
- package/src/components/mention-input/mention-input.tsx +66 -46
- package/src/components/mention-input/mention-value.ts +8 -2
- package/src/components/menubar/menubar.tsx +16 -6
- package/src/components/metric-card/metric-card.stories.tsx +5 -3
- package/src/components/metric-card/metric-card.tsx +21 -2
- package/src/components/model-picker/model-picker.stories.tsx +63 -3
- package/src/components/model-picker/model-picker.test.tsx +63 -0
- package/src/components/model-picker/model-picker.tsx +62 -36
- package/src/components/nav-main/nav-main.tsx +8 -4
- package/src/components/nav-notifications/nav-notifications.stories.tsx +110 -0
- package/src/components/nav-notifications/nav-notifications.test.tsx +11 -0
- package/src/components/nav-notifications/nav-notifications.tsx +67 -9
- package/src/components/nav-user/nav-user.tsx +5 -5
- package/src/components/navigation-menu/navigation-menu.stories.tsx +22 -0
- package/src/components/navigation-menu/navigation-menu.test.tsx +174 -0
- package/src/components/navigation-menu/navigation-menu.tsx +204 -15
- package/src/components/number-input/number-input.stories.tsx +7 -3
- package/src/components/number-input/number-input.test.tsx +47 -0
- package/src/components/number-input/number-input.tsx +65 -24
- package/src/components/page-shell/page-shell.stories.tsx +163 -0
- package/src/components/page-shell/page-shell.test.tsx +229 -0
- package/src/components/page-shell/page-shell.tsx +108 -25
- package/src/components/pagination/pagination.test.tsx +109 -0
- package/src/components/pagination/pagination.tsx +27 -19
- package/src/components/popover/popover.tsx +1 -0
- package/src/components/progress/progress.tsx +2 -0
- package/src/components/radio-group/radio-group.tsx +1 -1
- package/src/components/rating/rating.tsx +3 -1
- package/src/components/resizable/resizable.stories.tsx +114 -2
- package/src/components/resizable/resizable.test.tsx +76 -0
- package/src/components/resizable/resizable.tsx +32 -6
- package/src/components/revision-timeline/revision-timeline.stories.tsx +8 -1
- package/src/components/revision-timeline/revision-timeline.test.tsx +26 -0
- package/src/components/revision-timeline/revision-timeline.tsx +53 -5
- package/src/components/schema-form/from-json-schema.test.ts +390 -0
- package/src/components/schema-form/from-json-schema.ts +375 -0
- package/src/components/schema-form/index.ts +81 -0
- package/src/components/schema-form/schema-form-spec.ts +698 -0
- package/src/components/schema-form/schema-form-store.ts +506 -0
- package/src/components/schema-form/schema-form.stories.tsx +566 -0
- package/src/components/schema-form/schema-form.test.tsx +1021 -0
- package/src/components/schema-form/schema-form.tsx +1381 -0
- package/src/components/section-header/section-header.stories.tsx +37 -0
- package/src/components/section-header/section-header.tsx +14 -3
- package/src/components/select/select.tsx +13 -7
- package/src/components/separator/separator.tsx +1 -0
- package/src/components/sheet/sheet.test.tsx +39 -0
- package/src/components/sheet/sheet.tsx +40 -9
- package/src/components/side-dock/index.ts +1 -0
- package/src/components/side-dock/side-dock.stories.tsx +137 -0
- package/src/components/side-dock/side-dock.test.tsx +299 -0
- package/src/components/side-dock/side-dock.tsx +515 -0
- package/src/components/sidebar/index.ts +1 -0
- package/src/components/sidebar/sidebar-frame.test.tsx +162 -0
- package/src/components/sidebar/sidebar.stories.tsx +421 -1
- package/src/components/sidebar/sidebar.test.tsx +71 -2
- package/src/components/sidebar/sidebar.tsx +264 -38
- package/src/components/skeleton/skeleton.tsx +1 -0
- package/src/components/skip-link/index.ts +1 -0
- package/src/components/skip-link/skip-link.stories.tsx +106 -0
- package/src/components/skip-link/skip-link.test.tsx +20 -0
- package/src/components/skip-link/skip-link.tsx +34 -0
- package/src/components/slider/slider.tsx +1 -1
- package/src/components/slider-number/slider-number.stories.tsx +4 -1
- package/src/components/slider-number/slider-number.test.tsx +1 -1
- package/src/components/sonner/sonner.tsx +2 -0
- package/src/components/spinner/spinner.test.tsx +18 -0
- package/src/components/spinner/spinner.tsx +14 -4
- package/src/components/split-panel/index.ts +6 -1
- package/src/components/split-panel/split-panel.stories.tsx +18 -2
- package/src/components/split-panel/split-panel.test.tsx +20 -0
- package/src/components/split-panel/split-panel.tsx +28 -8
- package/src/components/state-panel/state-panel.stories.tsx +52 -0
- package/src/components/state-panel/state-panel.test.tsx +125 -0
- package/src/components/state-panel/state-panel.tsx +110 -24
- package/src/components/status-badge/status-badge.stories.tsx +8 -0
- package/src/components/switch/switch.tsx +3 -1
- package/src/components/table/table.stories.tsx +64 -0
- package/src/components/table/table.test.tsx +149 -1
- package/src/components/table/table.tsx +71 -2
- package/src/components/tabs/index.ts +10 -1
- package/src/components/tabs/tabs.stories.tsx +48 -0
- package/src/components/tabs/tabs.test.tsx +67 -0
- package/src/components/tabs/tabs.tsx +176 -72
- package/src/components/tag-input/tag-input.tsx +6 -3
- package/src/components/team-switcher/team-switcher.tsx +35 -6
- package/src/components/textarea/textarea.tsx +2 -2
- package/src/components/theme-switcher/theme-switcher.stories.tsx +76 -0
- package/src/components/theme-switcher/theme-switcher.test.tsx +182 -0
- package/src/components/theme-switcher/theme-switcher.tsx +140 -12
- package/src/components/theme-switcher/use-theme-transition.ts +2 -2
- package/src/components/timeline/timeline.stories.tsx +27 -0
- package/src/components/timeline/timeline.tsx +10 -3
- package/src/components/toggle/toggle.tsx +7 -2
- package/src/components/toggle-group/toggle-group.tsx +2 -0
- package/src/components/toolbar/toolbar.stories.tsx +10 -2
- package/src/components/tooltip/tooltip.tsx +2 -1
- package/src/components/top-nav/top-nav.tsx +1 -1
- package/src/components/transfer/transfer.tsx +35 -30
- package/src/components/tree/tree.stories.tsx +2 -2
- package/src/components/tree/tree.test.tsx +41 -0
- package/src/components/tree/tree.tsx +91 -31
- package/src/components/tree/use-tree-keyboard.ts +37 -14
- package/src/components/tree-select/tree-select.stories.tsx +9 -0
- package/src/components/tree-select/tree-select.test.tsx +5 -0
- package/src/components/tree-select/tree-select.tsx +11 -14
- package/src/components/typography/prose.test.tsx +3 -2
- package/src/components/typography/prose.tsx +3 -3
- package/src/components/typography/typography.stories.tsx +228 -0
- package/src/components/view-toolbar/view-toolbar.stories.tsx +41 -1
- package/src/components/view-toolbar/view-toolbar.test.tsx +33 -0
- package/src/components/view-toolbar/view-toolbar.tsx +37 -4
- package/src/components/virtual-select/virtual-select.stories.tsx +9 -0
- package/src/components/virtual-select/virtual-select.test.tsx +29 -3
- package/src/components/virtual-select/virtual-select.tsx +56 -18
- package/src/components/wizard/wizard.test.tsx +9 -0
- package/src/components/wizard/wizard.tsx +13 -5
- package/src/components/workspace-picker/index.ts +3 -0
- package/src/components/workspace-picker/workspace-picker-state.ts +50 -0
- package/src/components/workspace-picker/workspace-picker.stories.tsx +126 -0
- package/src/components/workspace-picker/workspace-picker.test.tsx +207 -0
- package/src/components/workspace-picker/workspace-picker.tsx +172 -0
- package/src/illustrations/empty-list-illustration.tsx +26 -0
- package/src/illustrations/error-illustration.tsx +60 -0
- package/src/illustrations/first-run-illustration.tsx +27 -0
- package/src/illustrations/illustration-base.tsx +108 -0
- package/src/illustrations/illustrations.stories.tsx +94 -0
- package/src/illustrations/illustrations.test.tsx +98 -0
- package/src/illustrations/index.ts +16 -0
- package/src/illustrations/no-access-illustration.tsx +45 -0
- package/src/illustrations/no-results-illustration.tsx +46 -0
- package/src/illustrations/offline-illustration.tsx +37 -0
- package/src/illustrations/success-illustration.tsx +40 -0
- package/src/index.ts +80 -2
- package/src/lib/agent-event-model.test.ts +12 -0
- package/src/lib/agent-event-model.ts +42 -0
- package/src/lib/approval-option.test.ts +17 -0
- package/src/lib/approval-option.ts +53 -0
- package/src/lib/check-result.ts +33 -0
- package/src/lib/cn.ts +9 -0
- package/src/lib/csv.ts +48 -0
- package/src/lib/diff-line.test.ts +36 -0
- package/src/lib/diff-line.ts +63 -0
- package/src/lib/diff-rows.test.ts +76 -0
- package/src/lib/diff-rows.ts +94 -0
- package/src/lib/format-duration.test.ts +22 -0
- package/src/lib/format-duration.ts +30 -0
- package/src/lib/has-renderable-content.test.ts +49 -0
- package/src/lib/has-renderable-content.ts +32 -0
- package/src/lib/merge-refs.ts +2 -0
- package/src/lib/operating-mode.test.ts +36 -0
- package/src/lib/operating-mode.ts +81 -0
- package/src/lib/optional-peer.ts +59 -0
- package/src/lib/session-launch.ts +37 -0
- package/src/lib/slash-command.test.ts +44 -0
- package/src/lib/slash-command.ts +45 -0
- package/src/lib/trigger-query.test.ts +94 -0
- package/src/lib/trigger-query.ts +88 -0
- package/src/lib/use-controllable-state.ts +47 -0
- package/src/lib/use-mobile.test.tsx +47 -0
- package/src/lib/use-mobile.ts +34 -12
- package/src/motion.stories.tsx +6 -0
- package/src/templates-object-detail-hub.stories.tsx +2 -2
- package/src/templates-screen-states.stories.tsx +5 -5
- package/src/templates-settings.stories.tsx +1 -1
- package/src/blocks/sidebar-02/app-sidebar.tsx +0 -160
- package/src/blocks/sidebar-02/logo.tsx +0 -18
- package/src/blocks/sidebar-02/nav-main.tsx +0 -13
- package/src/blocks/sidebar-02/nav-notifications.tsx +0 -7
- package/src/blocks/sidebar-02/sidebar-02.stories.tsx +0 -23
- package/src/blocks/sidebar-02/team-switcher.tsx +0 -7
- package/src/blocks/sidebar-04/app-sidebar.tsx +0 -236
- package/src/blocks/sidebar-04/mail-context.tsx +0 -29
- package/src/blocks/sidebar-04/nav-user.tsx +0 -6
- package/src/blocks/sidebar-04/sidebar-04.stories.tsx +0 -150
- package/src/blocks/sidebar-05/app-sidebar.tsx +0 -375
- package/src/blocks/sidebar-05/sidebar-05.stories.tsx +0 -18
- 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
|
-
|
|
299
|
+
{t("ui.changeReview.heading")}
|
|
266
300
|
</span>
|
|
267
301
|
<Badge variant={allApproved ? "success" : approvedCount > 0 ? "info" : "secondary"}>
|
|
268
|
-
{approvedCount
|
|
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="
|
|
313
|
+
aria-label={t("ui.changeReview.rejectAllLabel")}
|
|
280
314
|
>
|
|
281
315
|
<X aria-hidden="true" />
|
|
282
|
-
|
|
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="
|
|
323
|
+
aria-label={t("ui.changeReview.approveAllLabel")}
|
|
290
324
|
>
|
|
291
325
|
<CheckCheck aria-hidden="true" />
|
|
292
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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="
|
|
394
|
-
description="
|
|
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
|
-
|
|
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
|
|
434
|
-
added: "
|
|
435
|
-
removed: "
|
|
436
|
-
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
|
|
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 =
|
|
467
|
-
const toggleLabel = approved ?
|
|
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">
|
|
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">
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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";
|