@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
|
@@ -2,12 +2,14 @@ import { forwardRef, type ComponentProps } from "react";
|
|
|
2
2
|
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
|
|
3
3
|
import { cn } from "../../lib/cn";
|
|
4
4
|
import { buttonVariants } from "../button";
|
|
5
|
+
import { useLocale } from "../locale-provider";
|
|
5
6
|
|
|
6
7
|
export function Pagination({ className, ...props }: ComponentProps<"nav">) {
|
|
8
|
+
const { t } = useLocale();
|
|
7
9
|
return (
|
|
8
10
|
<nav
|
|
9
11
|
role="navigation"
|
|
10
|
-
aria-label="pagination"
|
|
12
|
+
aria-label={t("ui.pagination.label")}
|
|
11
13
|
className={cn("mx-auto flex w-full justify-center", className)}
|
|
12
14
|
{...props}
|
|
13
15
|
/>
|
|
@@ -26,44 +28,50 @@ export const PaginationItem = forwardRef<HTMLLIElement, ComponentProps<"li">>(
|
|
|
26
28
|
},
|
|
27
29
|
);
|
|
28
30
|
type PaginationLinkProps = { isActive?: boolean } & ComponentProps<"a">;
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
31
|
+
export const PaginationLink = forwardRef<HTMLAnchorElement, PaginationLinkProps>(
|
|
32
|
+
function PaginationLink({ className, isActive, ...props }, ref) {
|
|
33
|
+
return (
|
|
34
|
+
<a
|
|
35
|
+
ref={ref}
|
|
36
|
+
aria-current={isActive ? "page" : undefined}
|
|
37
|
+
className={cn(
|
|
38
|
+
buttonVariants({ variant: isActive ? "outline" : "ghost", size: "icon" }),
|
|
39
|
+
"cursor-pointer",
|
|
40
|
+
className,
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
);
|
|
42
47
|
export function PaginationPrevious({ className, ...props }: ComponentProps<"a">) {
|
|
48
|
+
const { t } = useLocale();
|
|
43
49
|
return (
|
|
44
50
|
<a
|
|
45
|
-
aria-label="
|
|
51
|
+
aria-label={t("ui.pagination.previous")}
|
|
46
52
|
className={cn(buttonVariants({ variant: "ghost" }), "gap-1 ps-2.5 cursor-pointer", className)}
|
|
47
53
|
{...props}
|
|
48
54
|
>
|
|
49
55
|
<ChevronLeft className="size-4" data-rtl-flip />
|
|
50
|
-
<span>
|
|
56
|
+
<span>{t("previous")}</span>
|
|
51
57
|
</a>
|
|
52
58
|
);
|
|
53
59
|
}
|
|
54
60
|
export function PaginationNext({ className, ...props }: ComponentProps<"a">) {
|
|
61
|
+
const { t } = useLocale();
|
|
55
62
|
return (
|
|
56
63
|
<a
|
|
57
|
-
aria-label="
|
|
64
|
+
aria-label={t("ui.pagination.next")}
|
|
58
65
|
className={cn(buttonVariants({ variant: "ghost" }), "gap-1 pe-2.5 cursor-pointer", className)}
|
|
59
66
|
{...props}
|
|
60
67
|
>
|
|
61
|
-
<span>
|
|
68
|
+
<span>{t("next")}</span>
|
|
62
69
|
<ChevronRight className="size-4" data-rtl-flip />
|
|
63
70
|
</a>
|
|
64
71
|
);
|
|
65
72
|
}
|
|
66
73
|
export function PaginationEllipsis({ className, ...props }: ComponentProps<"span">) {
|
|
74
|
+
const { t } = useLocale();
|
|
67
75
|
return (
|
|
68
76
|
<span
|
|
69
77
|
aria-hidden
|
|
@@ -71,7 +79,7 @@ export function PaginationEllipsis({ className, ...props }: ComponentProps<"span
|
|
|
71
79
|
{...props}
|
|
72
80
|
>
|
|
73
81
|
<MoreHorizontal className="size-4" />
|
|
74
|
-
<span className="sr-only">
|
|
82
|
+
<span className="sr-only">{t("ui.pagination.morePages")}</span>
|
|
75
83
|
</span>
|
|
76
84
|
);
|
|
77
85
|
}
|
|
@@ -36,11 +36,13 @@ export const Progress = forwardRef<ElementRef<typeof ProgressPrimitive.Root>, Pr
|
|
|
36
36
|
return (
|
|
37
37
|
<ProgressPrimitive.Root
|
|
38
38
|
ref={ref}
|
|
39
|
+
data-slot="progress"
|
|
39
40
|
value={value}
|
|
40
41
|
className={cn("relative h-2 w-full overflow-hidden rounded-full bg-muted", className)}
|
|
41
42
|
{...props}
|
|
42
43
|
>
|
|
43
44
|
<ProgressPrimitive.Indicator
|
|
45
|
+
data-slot="progress-indicator"
|
|
44
46
|
className={progressIndicatorVariants({ variant })}
|
|
45
47
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
|
46
48
|
/>
|
|
@@ -19,7 +19,7 @@ export const RadioGroupItem = forwardRef<
|
|
|
19
19
|
ref={ref}
|
|
20
20
|
className={cn(
|
|
21
21
|
"aspect-square size-4 rounded-full border border-input text-primary shadow-sm transition-colors duration-fast ease-standard",
|
|
22
|
-
"focus-
|
|
22
|
+
"focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
23
23
|
"disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary",
|
|
24
24
|
className,
|
|
25
25
|
)}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
// a2ui.exposed: yes
|
|
2
4
|
import {
|
|
3
5
|
forwardRef,
|
|
@@ -187,7 +189,7 @@ export const Rating = forwardRef<HTMLDivElement, RatingProps>(function Rating(
|
|
|
187
189
|
aria-checked={checked}
|
|
188
190
|
aria-label={`${index} ${index === 1 ? "star" : "stars"}`}
|
|
189
191
|
tabIndex={i === tabStopIndex ? 0 : -1}
|
|
190
|
-
className="rounded-sm focus-
|
|
192
|
+
className="rounded-sm focus-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background"
|
|
191
193
|
onClick={(e) =>
|
|
192
194
|
commit(valueFromPointer(e as unknown as PointerEvent<HTMLButtonElement>, index))
|
|
193
195
|
}
|
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, userEvent } from "storybook/test";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { splitPaneVariants } from "../split-panel";
|
|
2
5
|
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "./resizable";
|
|
3
6
|
const meta = {
|
|
4
7
|
title: "Layout/Resizable",
|
|
5
8
|
component: ResizablePanelGroup,
|
|
6
|
-
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: "padded",
|
|
12
|
+
docs: {
|
|
13
|
+
description: {
|
|
14
|
+
component:
|
|
15
|
+
"Panes the user can drag to resize — fixed two-pane layouts with per-pane surface " +
|
|
16
|
+
"tones are `Layout/SplitPanel`, see " +
|
|
17
|
+
"[Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs). " +
|
|
18
|
+
"The `react-resizable-panels` primitive set (`ResizablePanelGroup`/`ResizablePanel`/ " +
|
|
19
|
+
"`ResizableHandle`): n panes, percentage sizing, persisted layout. Apply " +
|
|
20
|
+
"`splitPaneVariants({ tone })` (exported from `Layout/SplitPanel`) to a `ResizablePanel` " +
|
|
21
|
+
"for the same `plain`/`muted`/`card` ground-offset tiering — see the `Tiered` story.",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
7
25
|
} satisfies Meta<typeof ResizablePanelGroup>;
|
|
8
26
|
export default meta;
|
|
9
27
|
type Story = StoryObj<typeof meta>;
|
|
@@ -13,7 +31,7 @@ export const Default: Story = {
|
|
|
13
31
|
<ResizablePanel defaultSize={40} className="flex items-center justify-center p-4 text-body">
|
|
14
32
|
List
|
|
15
33
|
</ResizablePanel>
|
|
16
|
-
<ResizableHandle withHandle />
|
|
34
|
+
<ResizableHandle withHandle aria-label="Resize the list and detail panels" />
|
|
17
35
|
<ResizablePanel
|
|
18
36
|
defaultSize={60}
|
|
19
37
|
className="flex items-center justify-center p-4 text-body text-muted-foreground"
|
|
@@ -22,4 +40,98 @@ export const Default: Story = {
|
|
|
22
40
|
</ResizablePanel>
|
|
23
41
|
</ResizablePanelGroup>
|
|
24
42
|
),
|
|
43
|
+
// #156 — the handle forwards `...props`, so `aria-label` reaches
|
|
44
|
+
// `PanelResizeHandle` and replaces the bare "separator, 50" announcement with a
|
|
45
|
+
// name that says which two regions it divides. Tabbing to it (rather than a
|
|
46
|
+
// direct `querySelector` focus call) exercises the same path a keyboard user
|
|
47
|
+
// takes, and the `ArrowRight` assertion locks that naming the handle didn't
|
|
48
|
+
// regress the keyboard resize behaviour that was already correct.
|
|
49
|
+
play: async ({ canvasElement }) => {
|
|
50
|
+
const handle = canvasElement.querySelector<HTMLElement>('[role="separator"]');
|
|
51
|
+
await expect(handle).not.toBeNull();
|
|
52
|
+
|
|
53
|
+
await userEvent.tab();
|
|
54
|
+
await expect(handle).toHaveFocus();
|
|
55
|
+
await expect(handle).toHaveAccessibleName("Resize the list and detail panels");
|
|
56
|
+
|
|
57
|
+
const panel = canvasElement.querySelector<HTMLElement>("[data-panel]");
|
|
58
|
+
const sizeBefore = panel!.getAttribute("data-panel-size");
|
|
59
|
+
await userEvent.keyboard("{ArrowRight}");
|
|
60
|
+
const sizeAfter = panel!.getAttribute("data-panel-size");
|
|
61
|
+
await expect(sizeAfter).not.toBe(sizeBefore);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The same `plain`/`muted`/`card` ground-offset tiering `SplitPanel` gives its
|
|
67
|
+
* `startTone`/`endTone`, composed onto draggable panels via the exported
|
|
68
|
+
* `splitPaneVariants` — so a user-resizable layout doesn't need a hand-rolled second
|
|
69
|
+
* copy of the tone classes. The play test drags the handle and asserts the panel's
|
|
70
|
+
* relative size (`data-panel-size`, react-resizable-panels' `flexGrow`-derived
|
|
71
|
+
* attribute) actually changed, not an exact pixel width — a headless drag is flaky
|
|
72
|
+
* against pixel deltas — and that both tone classes survive the resize.
|
|
73
|
+
*/
|
|
74
|
+
export const Tiered: Story = {
|
|
75
|
+
render: () => (
|
|
76
|
+
<ResizablePanelGroup direction="horizontal" className="h-48 max-w-xl rounded-lg border">
|
|
77
|
+
<ResizablePanel
|
|
78
|
+
defaultSize={40}
|
|
79
|
+
className={cn(
|
|
80
|
+
splitPaneVariants({ tone: "muted" }),
|
|
81
|
+
"flex items-center justify-center p-4 text-body",
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
List
|
|
85
|
+
</ResizablePanel>
|
|
86
|
+
<ResizableHandle withHandle aria-label="Resize the list and detail panels" />
|
|
87
|
+
<ResizablePanel
|
|
88
|
+
defaultSize={60}
|
|
89
|
+
className={cn(
|
|
90
|
+
splitPaneVariants({ tone: "card" }),
|
|
91
|
+
"flex items-center justify-center p-4 text-body text-muted-foreground",
|
|
92
|
+
)}
|
|
93
|
+
>
|
|
94
|
+
Detail
|
|
95
|
+
</ResizablePanel>
|
|
96
|
+
</ResizablePanelGroup>
|
|
97
|
+
),
|
|
98
|
+
play: async ({ canvasElement }) => {
|
|
99
|
+
const panels = canvasElement.querySelectorAll<HTMLElement>("[data-panel]");
|
|
100
|
+
const [startPanel, endPanel] = Array.from(panels);
|
|
101
|
+
await expect(startPanel).toBeDefined();
|
|
102
|
+
await expect(endPanel).toBeDefined();
|
|
103
|
+
const sizeBefore = startPanel!.getAttribute("data-panel-size");
|
|
104
|
+
|
|
105
|
+
// Tones apply before the drag.
|
|
106
|
+
await expect(startPanel!.className).toMatch(/bg-surface-muted/);
|
|
107
|
+
await expect(endPanel!.className).toMatch(/bg-card/);
|
|
108
|
+
await expect(endPanel!.className).toMatch(/shadow-sm/);
|
|
109
|
+
|
|
110
|
+
// `ResizablePanel` sets `style="overflow: hidden"` inline (react-resizable-panels);
|
|
111
|
+
// an inline style always wins the cascade over `splitPaneVariants`' `overflow-auto`
|
|
112
|
+
// class, so the resolved overflow stays `hidden` here — the class isn't fighting
|
|
113
|
+
// anything visually, it's simply inert on this element. Locked so a future
|
|
114
|
+
// react-resizable-panels upgrade that drops the inline style doesn't silently
|
|
115
|
+
// flip scroll behaviour unnoticed.
|
|
116
|
+
await expect(getComputedStyle(startPanel!).overflow).toBe("hidden");
|
|
117
|
+
|
|
118
|
+
const handle = canvasElement.querySelector<HTMLElement>('[role="separator"]');
|
|
119
|
+
await expect(handle).not.toBeNull();
|
|
120
|
+
const rect = handle!.getBoundingClientRect();
|
|
121
|
+
const y = rect.top + rect.height / 2;
|
|
122
|
+
await userEvent.pointer([
|
|
123
|
+
{ keys: "[MouseLeft>]", target: handle!, coords: { clientX: rect.left + 1, clientY: y } },
|
|
124
|
+
{ coords: { clientX: rect.left + 80, clientY: y } },
|
|
125
|
+
{ keys: "[/MouseLeft]" },
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
// The relative size changed — never assert an exact pixel width (flaky headless).
|
|
129
|
+
const sizeAfter = startPanel!.getAttribute("data-panel-size");
|
|
130
|
+
await expect(sizeAfter).not.toBe(sizeBefore);
|
|
131
|
+
|
|
132
|
+
// Tones still apply after the drag.
|
|
133
|
+
await expect(startPanel!.className).toMatch(/bg-surface-muted/);
|
|
134
|
+
await expect(endPanel!.className).toMatch(/bg-card/);
|
|
135
|
+
await expect(endPanel!.className).toMatch(/shadow-sm/);
|
|
136
|
+
},
|
|
25
137
|
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { render } from "@testing-library/react";
|
|
4
|
+
import type * as ResizablePrimitiveModule from "react-resizable-panels";
|
|
5
|
+
import type { ImperativePanelGroupHandle } from "react-resizable-panels";
|
|
6
|
+
|
|
7
|
+
const capturedProps: Record<string, unknown>[] = [];
|
|
8
|
+
|
|
9
|
+
vi.mock("react-resizable-panels", async (importOriginal) => {
|
|
10
|
+
const actual = await importOriginal<typeof ResizablePrimitiveModule>();
|
|
11
|
+
return {
|
|
12
|
+
...actual,
|
|
13
|
+
PanelResizeHandle: (props: Record<string, unknown>) => {
|
|
14
|
+
capturedProps.push(props);
|
|
15
|
+
return actual.PanelResizeHandle(props as never);
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Imported AFTER the mock so the module picked up above is the one in use.
|
|
21
|
+
const { ResizableHandle, ResizablePanel, ResizablePanelGroup } = await import("./resizable");
|
|
22
|
+
|
|
23
|
+
describe("Resizable — handle hit area is expanded without changing the visible line", () => {
|
|
24
|
+
it("keeps the visible line at 1px (w-px) while widening the pointer hit area via hitAreaMargins", () => {
|
|
25
|
+
capturedProps.length = 0;
|
|
26
|
+
const { container } = render(
|
|
27
|
+
<ResizablePanelGroup direction="horizontal" className="h-48 max-w-xl">
|
|
28
|
+
<ResizablePanel defaultSize={40}>List</ResizablePanel>
|
|
29
|
+
<ResizableHandle aria-label="Resize" />
|
|
30
|
+
<ResizablePanel defaultSize={60}>Detail</ResizablePanel>
|
|
31
|
+
</ResizablePanelGroup>,
|
|
32
|
+
);
|
|
33
|
+
const handle = container.querySelector('[role="separator"]');
|
|
34
|
+
expect(handle).not.toBeNull();
|
|
35
|
+
// Visual line unchanged — still the 1px hairline, not a wider drawn box.
|
|
36
|
+
expect(handle!.className).toMatch(/\bw-px\b/);
|
|
37
|
+
|
|
38
|
+
// The DRAG hit area comes from the primitive's own pointer-distance
|
|
39
|
+
// config, not from CSS: at least 16px combined margin around the line
|
|
40
|
+
// (>= 8px each side), satisfying the ≥16px minimum without widening
|
|
41
|
+
// anything visible.
|
|
42
|
+
const props = capturedProps.at(-1);
|
|
43
|
+
const margins = props?.hitAreaMargins as { coarse: number; fine: number } | undefined;
|
|
44
|
+
expect(margins).toBeDefined();
|
|
45
|
+
expect(margins!.fine).toBeGreaterThanOrEqual(8);
|
|
46
|
+
expect(margins!.coarse).toBeGreaterThanOrEqual(8);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("lets a caller override hitAreaMargins explicitly", () => {
|
|
50
|
+
capturedProps.length = 0;
|
|
51
|
+
render(
|
|
52
|
+
<ResizablePanelGroup direction="horizontal" className="h-48 max-w-xl">
|
|
53
|
+
<ResizablePanel defaultSize={40}>List</ResizablePanel>
|
|
54
|
+
<ResizableHandle aria-label="Resize" hitAreaMargins={{ coarse: 20, fine: 20 }} />
|
|
55
|
+
<ResizablePanel defaultSize={60}>Detail</ResizablePanel>
|
|
56
|
+
</ResizablePanelGroup>,
|
|
57
|
+
);
|
|
58
|
+
const props = capturedProps.at(-1);
|
|
59
|
+
expect(props?.hitAreaMargins).toEqual({ coarse: 20, fine: 20 });
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("Resizable — forwardRef", () => {
|
|
64
|
+
it("ResizablePanelGroup forwards its ref to the imperative panel-group handle", () => {
|
|
65
|
+
const ref = createRef<ImperativePanelGroupHandle>();
|
|
66
|
+
render(
|
|
67
|
+
<ResizablePanelGroup ref={ref} direction="horizontal" className="h-48 max-w-xl">
|
|
68
|
+
<ResizablePanel defaultSize={40}>List</ResizablePanel>
|
|
69
|
+
<ResizableHandle aria-label="Resize" />
|
|
70
|
+
<ResizablePanel defaultSize={60}>Detail</ResizablePanel>
|
|
71
|
+
</ResizablePanelGroup>,
|
|
72
|
+
);
|
|
73
|
+
expect(ref.current).not.toBeNull();
|
|
74
|
+
expect(typeof ref.current?.getLayout).toBe("function");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -1,30 +1,56 @@
|
|
|
1
|
+
import { forwardRef, type ForwardRefExoticComponent, type RefAttributes } from "react";
|
|
1
2
|
import { GripVertical } from "lucide-react";
|
|
2
3
|
import * as ResizablePrimitive from "react-resizable-panels";
|
|
3
4
|
import { cn } from "../../lib/cn";
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
export const ResizablePanelGroup: ForwardRefExoticComponent<
|
|
7
|
+
React.ComponentProps<typeof ResizablePrimitive.PanelGroup> &
|
|
8
|
+
RefAttributes<ResizablePrimitive.ImperativePanelGroupHandle>
|
|
9
|
+
> = forwardRef<
|
|
10
|
+
ResizablePrimitive.ImperativePanelGroupHandle,
|
|
11
|
+
React.ComponentProps<typeof ResizablePrimitive.PanelGroup>
|
|
12
|
+
>(function ResizablePanelGroup({ className, ...props }, ref) {
|
|
9
13
|
return (
|
|
10
14
|
<ResizablePrimitive.PanelGroup
|
|
15
|
+
ref={ref}
|
|
11
16
|
className={cn("flex size-full data-[panel-group-direction=vertical]:flex-col", className)}
|
|
12
17
|
{...props}
|
|
13
18
|
/>
|
|
14
19
|
);
|
|
15
|
-
}
|
|
20
|
+
});
|
|
16
21
|
|
|
17
22
|
export const ResizablePanel = ResizablePrimitive.Panel;
|
|
18
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The visible line stays 1px (`w-px`); the DRAG hit area is expanded via the
|
|
26
|
+
* primitive's own `hitAreaMargins` (pointer-distance based, not a CSS trick),
|
|
27
|
+
* so pointer/touch users get a ≥16px target without widening what's drawn.
|
|
28
|
+
* Callers can still override `hitAreaMargins` explicitly.
|
|
29
|
+
*/
|
|
30
|
+
const DEFAULT_HIT_AREA_MARGINS: ResizablePrimitive.PointerHitAreaMargins = {
|
|
31
|
+
coarse: 15,
|
|
32
|
+
fine: 8,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* NOT wrapped in `forwardRef`: `react-resizable-panels`' `PanelResizeHandle`
|
|
37
|
+
* is a plain function component that hardcodes its own internal element ref
|
|
38
|
+
* and does not accept `ref` as a prop at all (neither in its types nor at
|
|
39
|
+
* runtime — verified: a `ref` passed to it resolves to `null`). There is no
|
|
40
|
+
* DOM node here for a wrapper ref to attach to without changing what render
|
|
41
|
+
* onto the actual drag surface, so adding `forwardRef` would be a dead prop.
|
|
42
|
+
*/
|
|
19
43
|
export function ResizableHandle({
|
|
20
44
|
withHandle,
|
|
21
45
|
className,
|
|
46
|
+
hitAreaMargins,
|
|
22
47
|
...props
|
|
23
48
|
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & { withHandle?: boolean }) {
|
|
24
49
|
return (
|
|
25
50
|
<ResizablePrimitive.PanelResizeHandle
|
|
51
|
+
hitAreaMargins={hitAreaMargins ?? DEFAULT_HIT_AREA_MARGINS}
|
|
26
52
|
className={cn(
|
|
27
|
-
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-
|
|
53
|
+
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-ring",
|
|
28
54
|
"data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0",
|
|
29
55
|
className,
|
|
30
56
|
)}
|
|
@@ -17,10 +17,17 @@ const meta = {
|
|
|
17
17
|
docs: {
|
|
18
18
|
description: {
|
|
19
19
|
component:
|
|
20
|
+
"The GIT-HISTORY rail; a generic ordered-steps rail is `Core/Timeline` — see " +
|
|
21
|
+
"[Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs). " +
|
|
20
22
|
"A presentational git-history timeline. Renders revisions (newest-first) with " +
|
|
21
23
|
"day grouping, an SVG branch-graph lane gutter, churn indicators (+/−), " +
|
|
22
24
|
"ref chips, and an accessible selection rail. The app computes lane indices " +
|
|
23
|
-
"and edges; the component only renders what it receives."
|
|
25
|
+
"and edges; the component only renders what it receives. " +
|
|
26
|
+
"It deliberately does not ride the shared `Timeline` rail: a rail node's colour " +
|
|
27
|
+
"there is the closed 7-state execution `Status`, while a node here is coloured by " +
|
|
28
|
+
"lane identity (the chart ramp), and a fork/merge join is a cross-row bezier that " +
|
|
29
|
+
"no item-local connector can draw — the four-point reason is in the component's " +
|
|
30
|
+
"docblock (RM-014).",
|
|
24
31
|
},
|
|
25
32
|
},
|
|
26
33
|
},
|
|
@@ -264,6 +264,32 @@ describe("Merge rows", () => {
|
|
|
264
264
|
expect(svg).not.toBeNull();
|
|
265
265
|
});
|
|
266
266
|
|
|
267
|
+
// #387 — colour is never the only channel (WCAG 1.4.1). The lane graph paints
|
|
268
|
+
// every node from the same categorical chart ramp, so the ROW GLYPH is the cue
|
|
269
|
+
// that survives greyscale: the merge-marked row renders `GitMerge`, every other
|
|
270
|
+
// row `GitCommitHorizontal`. Asserted on the rendered `lucide-*` class (the
|
|
271
|
+
// actual shape signature) — the idiom flow-node.test.tsx uses. This is also the
|
|
272
|
+
// non-colour channel that justifies this component keeping its own node
|
|
273
|
+
// vocabulary instead of the shared `Timeline` rail's status-keyed dot (RM-014,
|
|
274
|
+
// #133) — see the component docblock.
|
|
275
|
+
//
|
|
276
|
+
// WHICH row is marked is deliberately NOT asserted here: the derivation counts
|
|
277
|
+
// edges by `to` (parents with >=2 children), so today the glyph lands on the
|
|
278
|
+
// fork point rather than on the commit whose message says "Merge". That routing
|
|
279
|
+
// question is out of scope for RM-014 and is reported separately; this lock is
|
|
280
|
+
// about the CHANNEL, and must keep holding whichever row ends up marked.
|
|
281
|
+
it("marks the merge row with a distinct glyph, not colour alone (#387)", () => {
|
|
282
|
+
render(<RevisionTimeline revisions={MERGE_REVISIONS} edges={MERGE_EDGES} groupBy="none" />);
|
|
283
|
+
const rows = Array.from(document.querySelectorAll("[data-revision-id]"));
|
|
284
|
+
expect(rows).toHaveLength(MERGE_REVISIONS.length);
|
|
285
|
+
const merge = rows.filter((r) => r.querySelector("svg.lucide-git-merge"));
|
|
286
|
+
const plain = rows.filter((r) => r.querySelector("svg.lucide-git-commit-horizontal"));
|
|
287
|
+
expect(merge).toHaveLength(1);
|
|
288
|
+
expect(plain).toHaveLength(rows.length - 1);
|
|
289
|
+
// No row carries both shapes, so the two are genuinely exclusive silhouettes.
|
|
290
|
+
expect(merge[0]!.querySelector("svg.lucide-git-commit-horizontal")).toBeNull();
|
|
291
|
+
});
|
|
292
|
+
|
|
267
293
|
it("renders all revisions including the merge commit", () => {
|
|
268
294
|
render(<RevisionTimeline revisions={MERGE_REVISIONS} edges={MERGE_EDGES} groupBy="none" />);
|
|
269
295
|
expect(screen.getAllByRole("listitem")).toHaveLength(MERGE_REVISIONS.length);
|
|
@@ -12,6 +12,49 @@
|
|
|
12
12
|
* The APP computes the graph layout (lane indices, edges). This component
|
|
13
13
|
* only renders what it receives. Heavy graph-math stays out.
|
|
14
14
|
*
|
|
15
|
+
* WHY THIS DOES NOT RIDE THE SHARED `Timeline` RAIL (RM-014, #133).
|
|
16
|
+
* `components/timeline/` is the canonical rail/node/connector spine (#190) and
|
|
17
|
+
* `AgentTimeline` rides it. This component deliberately does not. The decision
|
|
18
|
+
* was taken against the real code, so do not re-investigate it and do not
|
|
19
|
+
* "converge" the two without reopening #133 — four things block it:
|
|
20
|
+
*
|
|
21
|
+
* 1. The node channel already carries a DIFFERENT meaning there. On the
|
|
22
|
+
* shared rail a node's colour IS the closed 7-state execution `Status`:
|
|
23
|
+
* `NODE_STYLE` is locked to `STATUS_ROLE`/`statusBadgeVariants` (#392)
|
|
24
|
+
* and every status owns a unique non-colour signature (#387). A node
|
|
25
|
+
* here is coloured by LANE IDENTITY (`laneColor` → `--chart-1..5`, a
|
|
26
|
+
* categorical series ramp), and a revision has no execution status at
|
|
27
|
+
* all. Mapping lane → status would be a lie (lane 3 is not `failed`),
|
|
28
|
+
* and opening the rail node to a free-form colour would dissolve the two
|
|
29
|
+
* invariants those tests exist to defend.
|
|
30
|
+
* 2. A cross-lane join cannot be an item-local connector. The shared rail's
|
|
31
|
+
* connector is one `w-px` span inside each `<li>` at a fixed inline
|
|
32
|
+
* offset. A fork/merge join here is a cubic bezier from one row's centre
|
|
33
|
+
* to ANOTHER row's centre, several rows away and in a different lane —
|
|
34
|
+
* geometry that only exists in a coordinate space spanning the whole
|
|
35
|
+
* list (`rowCenterY` + `LaneGutter`). No per-item primitive can draw it,
|
|
36
|
+
* so the reusable unit would have to be the list, not the item.
|
|
37
|
+
* 3. Row geometry is load-bearing, not styling. Rows are fixed-height
|
|
38
|
+
* (`ROW_H_COMFORTABLE`/`ROW_H_COMPACT`) and day headers fixed at
|
|
39
|
+
* `DAY_HEADER_H` precisely because the SVG shares the list's coordinate
|
|
40
|
+
* space; the shared rail item is content-height (`pb-5 ps-7 last:pb-0`).
|
|
41
|
+
* Riding it means overriding every geometry decision it makes.
|
|
42
|
+
* 4. The row is a selection control, not a title slot. The whole row is one
|
|
43
|
+
* `<button aria-pressed>`, while the rail item's title slot is a span
|
|
44
|
+
* that unconditionally prefixes an `sr-only` status word — so every
|
|
45
|
+
* revision would announce a status it does not have.
|
|
46
|
+
*
|
|
47
|
+
* Teaching the rail all four means ~5 opt-in props, four of which turn OFF
|
|
48
|
+
* what the spine provides (its node, its connector, its geometry, its status
|
|
49
|
+
* announcement) and one of which moves `LaneGutter` into it — a behavioural
|
|
50
|
+
* mode fork (.claude/rules/component-api.md) paid for by every existing rail
|
|
51
|
+
* consumer. That relocates the duplication rather than removing it. The two
|
|
52
|
+
* are one silhouette ("a vertical list with dots and lines") speaking two
|
|
53
|
+
* grammars: an execution trace vs a commit DAG.
|
|
54
|
+
*
|
|
55
|
+
* Nothing visual is shared, but the fork-prevention gate still binds this
|
|
56
|
+
* file — which is why the GATE NOTES below are not optional.
|
|
57
|
+
*
|
|
15
58
|
* GATE NOTES:
|
|
16
59
|
* - No identifier starting with `Timeline` (timeline-fork class 1).
|
|
17
60
|
* - Lanes/connectors drawn with SVG <line> / <path>, NOT `absolute w-px`
|
|
@@ -30,6 +73,7 @@ import {
|
|
|
30
73
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
31
74
|
import { ChevronRight, GitBranch, GitCommitHorizontal, GitMerge, Plus, Minus } from "lucide-react";
|
|
32
75
|
import { cn } from "../../lib/cn";
|
|
76
|
+
import { useLocale } from "../locale-provider";
|
|
33
77
|
|
|
34
78
|
// ─── Public data types ────────────────────────────────────────────────────────
|
|
35
79
|
|
|
@@ -566,7 +610,7 @@ function BranchToggle({ branchName, onToggle }: { branchName: string; onToggle:
|
|
|
566
610
|
"flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground",
|
|
567
611
|
"transition-colors duration-fast ease-standard motion-reduce:transition-none",
|
|
568
612
|
"hover:bg-accent hover:text-accent-foreground",
|
|
569
|
-
"focus-
|
|
613
|
+
"focus-ring",
|
|
570
614
|
)}
|
|
571
615
|
>
|
|
572
616
|
<BranchChevron expanded />
|
|
@@ -615,7 +659,7 @@ function CollapsedBranchRow({
|
|
|
615
659
|
className={cn(
|
|
616
660
|
"flex h-full w-full min-w-0 items-center gap-2 px-2 text-start",
|
|
617
661
|
"transition-colors duration-fast ease-standard motion-reduce:transition-none",
|
|
618
|
-
"rounded focus-
|
|
662
|
+
"rounded focus-ring",
|
|
619
663
|
"hover:bg-accent hover:text-accent-foreground",
|
|
620
664
|
)}
|
|
621
665
|
>
|
|
@@ -695,7 +739,7 @@ function RevisionRow({ revision, isMerge, density, indent, collapseToggle }: Rev
|
|
|
695
739
|
? "flex h-full min-w-0 flex-1 items-center gap-2 px-2 text-start"
|
|
696
740
|
: "flex h-full w-full min-w-0 items-center gap-2 px-2 text-start",
|
|
697
741
|
"transition-colors duration-fast ease-standard motion-reduce:transition-none",
|
|
698
|
-
"rounded focus-
|
|
742
|
+
"rounded focus-ring",
|
|
699
743
|
"hover:bg-accent hover:text-accent-foreground",
|
|
700
744
|
isSelected && "border-s-2 border-s-primary bg-accent text-accent-foreground",
|
|
701
745
|
)}
|
|
@@ -894,6 +938,7 @@ export const RevisionTimeline = forwardRef<HTMLDivElement, RevisionTimelineProps
|
|
|
894
938
|
},
|
|
895
939
|
ref,
|
|
896
940
|
) {
|
|
941
|
+
const { t } = useLocale();
|
|
897
942
|
const resolvedDensity: RevisionTimelineDensity = density ?? "comfortable";
|
|
898
943
|
|
|
899
944
|
// Collapse/expand mode is opt-in: active only when `branches` are supplied.
|
|
@@ -1016,7 +1061,7 @@ export const RevisionTimeline = forwardRef<HTMLDivElement, RevisionTimelineProps
|
|
|
1016
1061
|
const from = summaryOf.get(e.from) ?? e.from;
|
|
1017
1062
|
const to = summaryOf.get(e.to) ?? e.to;
|
|
1018
1063
|
if (from === to) continue; // collapsed into a single node
|
|
1019
|
-
const key = `${from}
|
|
1064
|
+
const key = `${from}\u0000${to}`;
|
|
1020
1065
|
if (seen.has(key)) continue; // de-dup coincident remaps
|
|
1021
1066
|
seen.add(key);
|
|
1022
1067
|
out.push({ from, to });
|
|
@@ -1059,7 +1104,10 @@ export const RevisionTimeline = forwardRef<HTMLDivElement, RevisionTimelineProps
|
|
|
1059
1104
|
{groups.map((group) => (
|
|
1060
1105
|
<div key={group.dayKey}>
|
|
1061
1106
|
{groupBy === "day" && <DayHeader label={group.dayLabel} />}
|
|
1062
|
-
<ol
|
|
1107
|
+
<ol
|
|
1108
|
+
role="list"
|
|
1109
|
+
aria-label={groupBy === "day" ? group.dayLabel : t("ui.revisionTimeline.label")}
|
|
1110
|
+
>
|
|
1063
1111
|
{group.items.map((it) => {
|
|
1064
1112
|
if (it.kind === "summary") {
|
|
1065
1113
|
return (
|