@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
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
4
|
+
import { DirectionProvider } from "@radix-ui/react-direction";
|
|
5
|
+
import {
|
|
6
|
+
Carousel,
|
|
7
|
+
CarouselContent,
|
|
8
|
+
CarouselItem,
|
|
9
|
+
CarouselNext,
|
|
10
|
+
CarouselPrevious,
|
|
11
|
+
} from "./carousel";
|
|
12
|
+
|
|
13
|
+
function renderCarousel(props: Partial<React.ComponentProps<typeof Carousel>> = {}) {
|
|
14
|
+
return render(
|
|
15
|
+
<Carousel {...props}>
|
|
16
|
+
<CarouselContent>
|
|
17
|
+
<CarouselItem>
|
|
18
|
+
<input aria-label="slide input" defaultValue="abc" />
|
|
19
|
+
</CarouselItem>
|
|
20
|
+
<CarouselItem>Slide 2</CarouselItem>
|
|
21
|
+
</CarouselContent>
|
|
22
|
+
<CarouselPrevious />
|
|
23
|
+
<CarouselNext />
|
|
24
|
+
</Carousel>,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("Carousel keyboard handling", () => {
|
|
29
|
+
it("does not intercept ArrowLeft/ArrowRight when the target is a text input", () => {
|
|
30
|
+
renderCarousel();
|
|
31
|
+
const input = screen.getByLabelText("slide input") as HTMLInputElement;
|
|
32
|
+
input.focus();
|
|
33
|
+
const event = fireEvent.keyDown(input, { key: "ArrowRight", bubbles: true });
|
|
34
|
+
// `fireEvent` returns `false` from `dispatchEvent` only when
|
|
35
|
+
// `preventDefault()` was called — the carousel must leave the caret event
|
|
36
|
+
// alone so it reaches the input's own default behavior.
|
|
37
|
+
expect(event).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("still intercepts ArrowLeft/ArrowRight outside of an editable target", () => {
|
|
41
|
+
renderCarousel();
|
|
42
|
+
const region = screen.getByRole("region");
|
|
43
|
+
const event = fireEvent.keyDown(region, { key: "ArrowRight", bubbles: true });
|
|
44
|
+
expect(event).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("Carousel RTL keyboard mapping", () => {
|
|
49
|
+
it("flips ArrowLeft/ArrowRight to the visually-adjacent slide under dir=rtl", () => {
|
|
50
|
+
let scrollPrevSpy: ReturnType<typeof vi.spyOn> | undefined;
|
|
51
|
+
let scrollNextSpy: ReturnType<typeof vi.spyOn> | undefined;
|
|
52
|
+
render(
|
|
53
|
+
<DirectionProvider dir="rtl">
|
|
54
|
+
<Carousel
|
|
55
|
+
setApi={(api) => {
|
|
56
|
+
if (!api) return;
|
|
57
|
+
scrollPrevSpy = vi.spyOn(api, "scrollPrev");
|
|
58
|
+
scrollNextSpy = vi.spyOn(api, "scrollNext");
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
<CarouselContent>
|
|
62
|
+
<CarouselItem>Slide 1</CarouselItem>
|
|
63
|
+
<CarouselItem>Slide 2</CarouselItem>
|
|
64
|
+
</CarouselContent>
|
|
65
|
+
</Carousel>
|
|
66
|
+
</DirectionProvider>,
|
|
67
|
+
);
|
|
68
|
+
const region = screen.getByRole("region");
|
|
69
|
+
// In RTL, ArrowLeft is the visually-forward direction — it must trigger
|
|
70
|
+
// the SAME embla call the "Next slide" button does, not "Previous".
|
|
71
|
+
fireEvent.keyDown(region, { key: "ArrowLeft" });
|
|
72
|
+
expect(scrollNextSpy).toHaveBeenCalledTimes(1);
|
|
73
|
+
expect(scrollPrevSpy).not.toHaveBeenCalled();
|
|
74
|
+
|
|
75
|
+
fireEvent.keyDown(region, { key: "ArrowRight" });
|
|
76
|
+
expect(scrollPrevSpy).toHaveBeenCalledTimes(1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("keeps the physical mapping under dir=ltr (default)", () => {
|
|
80
|
+
let scrollPrevSpy: ReturnType<typeof vi.spyOn> | undefined;
|
|
81
|
+
let scrollNextSpy: ReturnType<typeof vi.spyOn> | undefined;
|
|
82
|
+
render(
|
|
83
|
+
<Carousel
|
|
84
|
+
setApi={(api) => {
|
|
85
|
+
if (!api) return;
|
|
86
|
+
scrollPrevSpy = vi.spyOn(api, "scrollPrev");
|
|
87
|
+
scrollNextSpy = vi.spyOn(api, "scrollNext");
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<CarouselContent>
|
|
91
|
+
<CarouselItem>Slide 1</CarouselItem>
|
|
92
|
+
<CarouselItem>Slide 2</CarouselItem>
|
|
93
|
+
</CarouselContent>
|
|
94
|
+
</Carousel>,
|
|
95
|
+
);
|
|
96
|
+
const region = screen.getByRole("region");
|
|
97
|
+
fireEvent.keyDown(region, { key: "ArrowRight" });
|
|
98
|
+
expect(scrollNextSpy).toHaveBeenCalledTimes(1);
|
|
99
|
+
fireEvent.keyDown(region, { key: "ArrowLeft" });
|
|
100
|
+
expect(scrollPrevSpy).toHaveBeenCalledTimes(1);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("uses ArrowUp/ArrowDown instead of ArrowLeft/ArrowRight for a vertical carousel", () => {
|
|
104
|
+
let scrollPrevSpy: ReturnType<typeof vi.spyOn> | undefined;
|
|
105
|
+
let scrollNextSpy: ReturnType<typeof vi.spyOn> | undefined;
|
|
106
|
+
render(
|
|
107
|
+
<Carousel
|
|
108
|
+
orientation="vertical"
|
|
109
|
+
setApi={(api) => {
|
|
110
|
+
if (!api) return;
|
|
111
|
+
scrollPrevSpy = vi.spyOn(api, "scrollPrev");
|
|
112
|
+
scrollNextSpy = vi.spyOn(api, "scrollNext");
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
<CarouselContent>
|
|
116
|
+
<CarouselItem>Slide 1</CarouselItem>
|
|
117
|
+
<CarouselItem>Slide 2</CarouselItem>
|
|
118
|
+
</CarouselContent>
|
|
119
|
+
</Carousel>,
|
|
120
|
+
);
|
|
121
|
+
const region = screen.getByRole("region");
|
|
122
|
+
fireEvent.keyDown(region, { key: "ArrowRight" });
|
|
123
|
+
expect(scrollNextSpy).not.toHaveBeenCalled();
|
|
124
|
+
fireEvent.keyDown(region, { key: "ArrowDown" });
|
|
125
|
+
expect(scrollNextSpy).toHaveBeenCalledTimes(1);
|
|
126
|
+
fireEvent.keyDown(region, { key: "ArrowUp" });
|
|
127
|
+
expect(scrollPrevSpy).toHaveBeenCalledTimes(1);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("CarouselPrevious / CarouselNext", () => {
|
|
132
|
+
it("forward a ref to the underlying button", () => {
|
|
133
|
+
const prevRef = createRef<HTMLButtonElement>();
|
|
134
|
+
const nextRef = createRef<HTMLButtonElement>();
|
|
135
|
+
renderCarousel();
|
|
136
|
+
render(
|
|
137
|
+
<Carousel>
|
|
138
|
+
<CarouselContent>
|
|
139
|
+
<CarouselItem>1</CarouselItem>
|
|
140
|
+
</CarouselContent>
|
|
141
|
+
<CarouselPrevious ref={prevRef} />
|
|
142
|
+
<CarouselNext ref={nextRef} />
|
|
143
|
+
</Carousel>,
|
|
144
|
+
);
|
|
145
|
+
expect(prevRef.current).toBeInstanceOf(HTMLButtonElement);
|
|
146
|
+
expect(nextRef.current).toBeInstanceOf(HTMLButtonElement);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("keeps the buttons inset on narrow viewports (no unconditional negative offset)", () => {
|
|
150
|
+
render(
|
|
151
|
+
<Carousel>
|
|
152
|
+
<CarouselContent>
|
|
153
|
+
<CarouselItem>1</CarouselItem>
|
|
154
|
+
</CarouselContent>
|
|
155
|
+
<CarouselPrevious />
|
|
156
|
+
<CarouselNext />
|
|
157
|
+
</Carousel>,
|
|
158
|
+
);
|
|
159
|
+
const prev = screen.getByRole("button", { name: "Previous slide" });
|
|
160
|
+
const next = screen.getByRole("button", { name: "Next slide" });
|
|
161
|
+
// The negative offset that pushes the button outside the carousel box
|
|
162
|
+
// only applies from `sm:` up; unconditionally offsetting it caused
|
|
163
|
+
// horizontal overflow on narrow phone screens.
|
|
164
|
+
expect(prev.className).not.toMatch(/(?<!sm:)-start-12/);
|
|
165
|
+
expect(next.className).not.toMatch(/(?<!sm:)-end-12/);
|
|
166
|
+
expect(prev.className).toContain("sm:-start-12");
|
|
167
|
+
expect(next.className).toContain("sm:-end-12");
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
createContext,
|
|
3
5
|
forwardRef,
|
|
@@ -6,13 +8,29 @@ import {
|
|
|
6
8
|
useEffect,
|
|
7
9
|
useState,
|
|
8
10
|
type ComponentProps,
|
|
11
|
+
type ElementRef,
|
|
9
12
|
type HTMLAttributes,
|
|
10
13
|
type KeyboardEvent,
|
|
11
14
|
} from "react";
|
|
15
|
+
import { useDirection } from "@radix-ui/react-direction";
|
|
12
16
|
import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
|
|
13
17
|
import { ArrowLeft, ArrowRight } from "lucide-react";
|
|
14
18
|
import { cn } from "../../lib/cn";
|
|
15
19
|
import { Button } from "../button";
|
|
20
|
+
import { useLocale } from "../locale-provider";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* True when the keyboard event's target already owns arrow-key semantics of
|
|
24
|
+
* its own (a text field's caret, a contenteditable's selection) — the
|
|
25
|
+
* carousel must not steal ArrowLeft/ArrowRight from a form control that
|
|
26
|
+
* happens to live inside the active slide.
|
|
27
|
+
*/
|
|
28
|
+
function isEditableTarget(target: EventTarget | null): boolean {
|
|
29
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
30
|
+
if (target.isContentEditable) return true;
|
|
31
|
+
const tag = target.tagName;
|
|
32
|
+
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
|
|
33
|
+
}
|
|
16
34
|
|
|
17
35
|
type CarouselApi = UseEmblaCarouselType[1];
|
|
18
36
|
type CarouselOptions = NonNullable<Parameters<typeof useEmblaCarousel>[0]>;
|
|
@@ -62,17 +80,20 @@ export const Carousel = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement
|
|
|
62
80
|
// aria-roledescription changes the announcement, it does not name the
|
|
63
81
|
// landmark. Default is overridable; pass a specific label ("Product
|
|
64
82
|
// photos") when the carousel's purpose isn't generic. Issue #279.
|
|
65
|
-
"aria-label": ariaLabel
|
|
83
|
+
"aria-label": ariaLabel,
|
|
66
84
|
...props
|
|
67
85
|
},
|
|
68
86
|
ref,
|
|
69
87
|
) {
|
|
88
|
+
const { t } = useLocale();
|
|
89
|
+
const resolvedAriaLabel = ariaLabel ?? t("ui.carousel.label");
|
|
70
90
|
const [carouselRef, api] = useEmblaCarousel(
|
|
71
91
|
{ ...opts, axis: orientation === "horizontal" ? "x" : "y" },
|
|
72
92
|
plugins,
|
|
73
93
|
);
|
|
74
94
|
const [canScrollPrev, setCanScrollPrev] = useState(false);
|
|
75
95
|
const [canScrollNext, setCanScrollNext] = useState(false);
|
|
96
|
+
const dir = useDirection();
|
|
76
97
|
|
|
77
98
|
const onSelect = useCallback((a: CarouselApi) => {
|
|
78
99
|
if (!a) return;
|
|
@@ -85,15 +106,42 @@ export const Carousel = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement
|
|
|
85
106
|
|
|
86
107
|
const onKeyDown = useCallback(
|
|
87
108
|
(e: KeyboardEvent<HTMLDivElement>) => {
|
|
109
|
+
// Don't steal ArrowLeft/ArrowRight from a text field's caret or a
|
|
110
|
+
// contenteditable's selection inside the active slide.
|
|
111
|
+
if (isEditableTarget(e.target)) return;
|
|
112
|
+
|
|
113
|
+
if (orientation === "vertical") {
|
|
114
|
+
if (e.key === "ArrowUp") {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
scrollPrev();
|
|
117
|
+
} else if (e.key === "ArrowDown") {
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
scrollNext();
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Horizontal: the PHYSICAL key maps to the visually-adjacent slide,
|
|
125
|
+
// which flips with writing direction — in RTL, ArrowLeft moves toward
|
|
126
|
+
// the next slide (visually to the left of the current one), not prev.
|
|
127
|
+
const isRtl = dir === "rtl";
|
|
88
128
|
if (e.key === "ArrowLeft") {
|
|
89
129
|
e.preventDefault();
|
|
90
|
-
|
|
130
|
+
if (isRtl) {
|
|
131
|
+
scrollNext();
|
|
132
|
+
} else {
|
|
133
|
+
scrollPrev();
|
|
134
|
+
}
|
|
91
135
|
} else if (e.key === "ArrowRight") {
|
|
92
136
|
e.preventDefault();
|
|
93
|
-
|
|
137
|
+
if (isRtl) {
|
|
138
|
+
scrollPrev();
|
|
139
|
+
} else {
|
|
140
|
+
scrollNext();
|
|
141
|
+
}
|
|
94
142
|
}
|
|
95
143
|
},
|
|
96
|
-
[scrollPrev, scrollNext],
|
|
144
|
+
[orientation, dir, scrollPrev, scrollNext],
|
|
97
145
|
);
|
|
98
146
|
|
|
99
147
|
useEffect(() => {
|
|
@@ -105,6 +153,7 @@ export const Carousel = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement
|
|
|
105
153
|
api.on("reInit", onSelect);
|
|
106
154
|
api.on("select", onSelect);
|
|
107
155
|
return () => {
|
|
156
|
+
api.off("reInit", onSelect);
|
|
108
157
|
api.off("select", onSelect);
|
|
109
158
|
};
|
|
110
159
|
}, [api, onSelect]);
|
|
@@ -128,7 +177,7 @@ export const Carousel = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement
|
|
|
128
177
|
className={cn("relative", className)}
|
|
129
178
|
role="region"
|
|
130
179
|
aria-roledescription="carousel"
|
|
131
|
-
aria-label={
|
|
180
|
+
aria-label={resolvedAriaLabel}
|
|
132
181
|
{...props}
|
|
133
182
|
>
|
|
134
183
|
{children}
|
|
@@ -176,20 +225,28 @@ export const CarouselItem = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivEle
|
|
|
176
225
|
},
|
|
177
226
|
);
|
|
178
227
|
|
|
179
|
-
export
|
|
228
|
+
export const CarouselPrevious = forwardRef<
|
|
229
|
+
ElementRef<typeof Button>,
|
|
230
|
+
ComponentProps<typeof Button>
|
|
231
|
+
>(function CarouselPrevious({ className, ...props }, ref) {
|
|
180
232
|
const { scrollPrev, canScrollPrev, orientation } = useCarousel();
|
|
233
|
+
const { t } = useLocale();
|
|
181
234
|
return (
|
|
182
235
|
<Button
|
|
236
|
+
ref={ref}
|
|
183
237
|
variant="outline"
|
|
184
238
|
size="icon"
|
|
185
239
|
disabled={!canScrollPrev}
|
|
186
240
|
onClick={scrollPrev}
|
|
187
|
-
aria-label="
|
|
241
|
+
aria-label={t("previousSlide")}
|
|
188
242
|
className={cn(
|
|
189
243
|
"absolute size-8 rounded-full",
|
|
244
|
+
// Inset on narrow screens (no room for the button to sit outside
|
|
245
|
+
// the carousel box without overflowing horizontally); pushed
|
|
246
|
+
// outside the box only from `sm` up, where there's margin for it.
|
|
190
247
|
orientation === "horizontal"
|
|
191
|
-
? "
|
|
192
|
-
: "
|
|
248
|
+
? "start-2 top-1/2 -translate-y-1/2 sm:-start-12"
|
|
249
|
+
: "top-2 left-1/2 -translate-x-1/2 rotate-90 sm:-top-12",
|
|
193
250
|
className,
|
|
194
251
|
)}
|
|
195
252
|
{...props}
|
|
@@ -197,29 +254,33 @@ export function CarouselPrevious({ className, ...props }: ComponentProps<typeof
|
|
|
197
254
|
<ArrowLeft className="size-4" data-rtl-flip />
|
|
198
255
|
</Button>
|
|
199
256
|
);
|
|
200
|
-
}
|
|
257
|
+
});
|
|
201
258
|
|
|
202
|
-
export
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
259
|
+
export const CarouselNext = forwardRef<ElementRef<typeof Button>, ComponentProps<typeof Button>>(
|
|
260
|
+
function CarouselNext({ className, ...props }, ref) {
|
|
261
|
+
const { scrollNext, canScrollNext, orientation } = useCarousel();
|
|
262
|
+
const { t } = useLocale();
|
|
263
|
+
return (
|
|
264
|
+
<Button
|
|
265
|
+
ref={ref}
|
|
266
|
+
variant="outline"
|
|
267
|
+
size="icon"
|
|
268
|
+
disabled={!canScrollNext}
|
|
269
|
+
onClick={scrollNext}
|
|
270
|
+
aria-label={t("nextSlide")}
|
|
271
|
+
className={cn(
|
|
272
|
+
"absolute size-8 rounded-full",
|
|
273
|
+
orientation === "horizontal"
|
|
274
|
+
? "end-2 top-1/2 -translate-y-1/2 sm:-end-12"
|
|
275
|
+
: "bottom-2 left-1/2 -translate-x-1/2 rotate-90 sm:-bottom-12",
|
|
276
|
+
className,
|
|
277
|
+
)}
|
|
278
|
+
{...props}
|
|
279
|
+
>
|
|
280
|
+
<ArrowRight className="size-4" data-rtl-flip />
|
|
281
|
+
</Button>
|
|
282
|
+
);
|
|
283
|
+
},
|
|
284
|
+
);
|
|
224
285
|
|
|
225
286
|
export type { CarouselApi };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
2
|
import { expect, userEvent, waitFor, within } from "storybook/test";
|
|
3
3
|
import { useState } from "react";
|
|
4
|
+
import { type CheckResult } from "../../lib/check-result";
|
|
4
5
|
import {
|
|
5
6
|
ChangeReview,
|
|
6
7
|
ChangeReviewHeader,
|
|
@@ -42,6 +43,41 @@ const PROVENANCE = {
|
|
|
42
43
|
note: "run-abc123",
|
|
43
44
|
};
|
|
44
45
|
|
|
46
|
+
// ─── Sample hunks with verification checks (#112) ─────────────────────────────
|
|
47
|
+
|
|
48
|
+
const HUNKS_WITH_CHECKS: ChangeHunk[] = [
|
|
49
|
+
{
|
|
50
|
+
id: "checked-1",
|
|
51
|
+
title: "src/api/handler.ts",
|
|
52
|
+
status: "modified",
|
|
53
|
+
before: `function handle(req) { return req.body; }`,
|
|
54
|
+
after: `function handle(req: Request) { return req.body; }`,
|
|
55
|
+
checks: [
|
|
56
|
+
{ label: "eslint", ok: true, durationMs: 214 },
|
|
57
|
+
{
|
|
58
|
+
label: "tsc",
|
|
59
|
+
ok: false,
|
|
60
|
+
durationMs: 890,
|
|
61
|
+
detail:
|
|
62
|
+
"src/api/handler.ts:1:31 — Parameter 'req' implicitly has an 'any' type, and the " +
|
|
63
|
+
"surrounding function type annotation does not resolve it. This is a deliberately " +
|
|
64
|
+
"long detail line so it renders behind a collapsed disclosure by default.",
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "checked-2",
|
|
70
|
+
title: "src/api/handler.test.ts",
|
|
71
|
+
status: "added",
|
|
72
|
+
after: `it("handles a request", () => { ... });`,
|
|
73
|
+
checks: [
|
|
74
|
+
{ label: "pre_tool_use", ok: true, phase: "before" },
|
|
75
|
+
{ label: "eslint", ok: true, phase: "after", durationMs: 120 },
|
|
76
|
+
{ label: "vitest", ok: true, phase: "after", detail: "3 passed", durationMs: 1420 },
|
|
77
|
+
] satisfies CheckResult[],
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
|
|
45
81
|
// ─── Meta ─────────────────────────────────────────────────────────────────────
|
|
46
82
|
|
|
47
83
|
const meta = {
|
|
@@ -53,10 +89,30 @@ const meta = {
|
|
|
53
89
|
docs: {
|
|
54
90
|
description: {
|
|
55
91
|
component:
|
|
92
|
+
"The ACCEPT/REJECT diff surface; read-only patch lines are `AI/DiffView` and an " +
|
|
93
|
+
"editable side-by-side is `Editor/DiffEditor` — see " +
|
|
94
|
+
"[Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs). " +
|
|
56
95
|
"AI-edit trust gate. When an agent proposes edits, the human reviews them " +
|
|
57
96
|
"hunk-by-hunk and accepts or rejects each before they apply. Compound " +
|
|
58
97
|
"component with lifted state — compose parts for custom layouts, or use the " +
|
|
59
|
-
"convenience `<ChangeReview>` root
|
|
98
|
+
"convenience `<ChangeReview>` root.\n\n" +
|
|
99
|
+
"**Ships from `@elabs-ai/components-ui`**, not from the package this story " +
|
|
100
|
+
"group names. Stories are grouped by what a component is *for*, which " +
|
|
101
|
+
"`docs/STORYBOOK_GUIDELINES.md` names as intentional and cites this component " +
|
|
102
|
+
"as the worked example.\n\n" +
|
|
103
|
+
"**A hunk’s `before` / `after` are prose, not diff lines.** They are ordinary " +
|
|
104
|
+
"`ReactNode`s, drawn as a deliberately quiet no-fill comparison — a struck " +
|
|
105
|
+
"muted “before” above a plain “after”, with the status rail carrying the " +
|
|
106
|
+
"polarity. This component is not a line renderer and is not going to become " +
|
|
107
|
+
"one.\n\n" +
|
|
108
|
+
"**Structured `DiffLine[]` hunks arrive by injection.** The app that depends " +
|
|
109
|
+
"on both packages passes a `<DiffView>` (`@elabs-ai/components-ai`) through " +
|
|
110
|
+
"the `renderHunk` render-prop or a hunk’s `after` slot; neither package " +
|
|
111
|
+
"imports the other. `ChangeHunk` gains no `lines` field and `DiffLine` " +
|
|
112
|
+
"never moves — a ratified seam, recorded in § 3 of the adoption-architecture " +
|
|
113
|
+
"decision. The *Injected hunk renderer* story below shows the shape with a " +
|
|
114
|
+
"stand-in node; the real wiring is *Composed into ChangeReview (ui)* on " +
|
|
115
|
+
"[AI/DiffView](?path=/docs/ai-diffview--docs).",
|
|
60
116
|
},
|
|
61
117
|
},
|
|
62
118
|
},
|
|
@@ -239,6 +295,55 @@ export const CustomRenderer: Story = {
|
|
|
239
295
|
},
|
|
240
296
|
};
|
|
241
297
|
|
|
298
|
+
// ─── Injected hunk renderer (the DiffView seam) ───────────────────────────────
|
|
299
|
+
//
|
|
300
|
+
// BINDING SEAM — docs/decisions/2026-09-01-brainless-adoption-architecture.md § 3.
|
|
301
|
+
// `ChangeHunk` gains no `lines` field and `DiffLine` never moves into this
|
|
302
|
+
// package. An app that wants structured patch rows inside the trust gate injects
|
|
303
|
+
// a renderer through `renderHunk` (or a hunk's `after` slot); in a real app that
|
|
304
|
+
// node is `<DiffView lines={…} />` from `@elabs-ai/components-ai`.
|
|
305
|
+
//
|
|
306
|
+
// This package has no dependency on that one in EITHER direction — that is the
|
|
307
|
+
// whole point of the seam — so the story stands in for it with a plain node of
|
|
308
|
+
// the same shape. The real composition is the `ChangeReviewComposition` story in
|
|
309
|
+
// `packages/ai/src/diff-view.stories.tsx`, which can import both.
|
|
310
|
+
|
|
311
|
+
function StandInLineRenderer({ file }: { file: string }) {
|
|
312
|
+
return (
|
|
313
|
+
<div
|
|
314
|
+
data-testid={`injected-${file}`}
|
|
315
|
+
className="rounded-md bg-surface-muted p-3 text-meta text-muted-foreground"
|
|
316
|
+
>
|
|
317
|
+
<span className="font-mono">{`<DiffView lines={…} />`}</span> renders the patch rows for{" "}
|
|
318
|
+
<span className="font-medium text-foreground">{file}</span> here.
|
|
319
|
+
</div>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export const InjectedHunkRenderer: Story = {
|
|
324
|
+
args: {
|
|
325
|
+
hunks: HUNKS,
|
|
326
|
+
renderHunk: (hunk) => <StandInLineRenderer file={hunk.title ?? hunk.id} />,
|
|
327
|
+
},
|
|
328
|
+
play: async ({ canvasElement }) => {
|
|
329
|
+
const canvas = within(canvasElement);
|
|
330
|
+
|
|
331
|
+
// The injected node replaces the default before/after prose on every hunk…
|
|
332
|
+
await waitFor(() => expect(canvas.getByTestId("injected-config/settings.json")).toBeVisible());
|
|
333
|
+
expect(canvas.getAllByTestId(/^injected-/)).toHaveLength(3);
|
|
334
|
+
|
|
335
|
+
// …and the default prose comparison is gone, because the seam replaces it.
|
|
336
|
+
expect(canvas.queryByText('"timeout": 30')).not.toBeInTheDocument();
|
|
337
|
+
|
|
338
|
+
// Approve/reject still belongs to ChangeReview, not to the injected renderer.
|
|
339
|
+
await userEvent.click(
|
|
340
|
+
canvas.getByRole("button", { name: /approve hunk: config\/settings\.json/i }),
|
|
341
|
+
);
|
|
342
|
+
await waitFor(() => expect(canvas.getByText("1 of 3 approved")).toBeVisible());
|
|
343
|
+
expect(canvas.getByTestId("injected-config/settings.json")).toBeVisible();
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
|
|
242
347
|
// ─── Compound (manual layout) ─────────────────────────────────────────────────
|
|
243
348
|
|
|
244
349
|
export const Compound: Story = {
|
|
@@ -286,3 +391,42 @@ edge cases properly and includes proper error boundaries.`,
|
|
|
286
391
|
],
|
|
287
392
|
},
|
|
288
393
|
};
|
|
394
|
+
|
|
395
|
+
// ─── With verification checks (#112) ──────────────────────────────────────────
|
|
396
|
+
// Evidence a reviewer needs (lint / types / tests / policy hooks) rendered
|
|
397
|
+
// alongside the diff. A failing check informs the decision — it never blocks
|
|
398
|
+
// approval, so `checked-1` (which carries a failing `tsc` check) can still be
|
|
399
|
+
// approved below.
|
|
400
|
+
|
|
401
|
+
export const WithChecks: Story = {
|
|
402
|
+
args: {
|
|
403
|
+
hunks: HUNKS_WITH_CHECKS,
|
|
404
|
+
},
|
|
405
|
+
play: async ({ canvasElement }) => {
|
|
406
|
+
const canvas = within(canvasElement);
|
|
407
|
+
await waitFor(() => expect(canvas.getByText("src/api/handler.ts")).toBeVisible());
|
|
408
|
+
|
|
409
|
+
// Passing and failing checks are distinguishable by accessible text —
|
|
410
|
+
// never by colour alone.
|
|
411
|
+
await waitFor(() => expect(canvas.getAllByText("Passed").length).toBeGreaterThan(0));
|
|
412
|
+
await waitFor(() => expect(canvas.getAllByText("Failed").length).toBeGreaterThan(0));
|
|
413
|
+
|
|
414
|
+
// The long `tsc` detail is collapsed by default…
|
|
415
|
+
const showDetail = canvas.getByRole("button", { name: /show detail/i });
|
|
416
|
+
expect(canvas.queryByText(/implicitly has an 'any' type/)).not.toBeInTheDocument();
|
|
417
|
+
// …and expands on click.
|
|
418
|
+
await userEvent.click(showDetail);
|
|
419
|
+
await waitFor(() => expect(canvas.getByText(/implicitly has an 'any' type/)).toBeVisible());
|
|
420
|
+
|
|
421
|
+
// Both phases present on the second hunk → grouped "Before" / "After".
|
|
422
|
+
await waitFor(() => expect(canvas.getByText("Before")).toBeVisible());
|
|
423
|
+
await waitFor(() => expect(canvas.getByText("After")).toBeVisible());
|
|
424
|
+
|
|
425
|
+
// A failing check does not block approval — it only informs it.
|
|
426
|
+
const approveFirstHunk = canvas.getByRole("button", {
|
|
427
|
+
name: /approve hunk: src\/api\/handler\.ts/i,
|
|
428
|
+
});
|
|
429
|
+
await userEvent.click(approveFirstHunk);
|
|
430
|
+
await waitFor(() => expect(canvas.getByText("1 of 2 approved")).toBeVisible());
|
|
431
|
+
},
|
|
432
|
+
};
|