@elabs-ai/components-ui 4.0.0 → 4.1.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-JKPPTL63.js +48 -0
- package/dist/chunk-JKPPTL63.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 +2485 -167
- package/dist/index.js +10716 -2706
- package/dist/index.js.map +1 -1
- package/package.json +21 -7
- package/src/_peer-deps.test.ts +59 -0
- package/src/blocks-comparison-table.stories.tsx +2 -2
- package/src/components/accordion/accordion.tsx +1 -1
- package/src/components/advanced-group/advanced-group.tsx +1 -1
- package/src/components/alert/alert.tsx +4 -1
- package/src/components/app-shell/app-shell.stories.tsx +44 -2
- package/src/components/app-shell/app-shell.tsx +35 -2
- package/src/components/app-sidebar/app-sidebar.stories.tsx +9 -1
- package/src/components/attribution-panel/attribution-panel.tsx +1 -1
- package/src/components/attribution-panel/attributions.generated.ts +81 -77
- package/src/components/badge/badge.stories.tsx +8 -0
- package/src/components/badge/badge.tsx +1 -1
- package/src/components/bento-grid/bento-grid.test.tsx +1 -1
- package/src/components/bento-grid/bento-grid.tsx +1 -1
- package/src/components/breadcrumb/breadcrumb.stories.tsx +38 -0
- package/src/components/breadcrumb/breadcrumb.test.tsx +24 -1
- package/src/components/breadcrumb/breadcrumb.tsx +7 -2
- package/src/components/button/button.stories.tsx +116 -8
- package/src/components/button/button.tsx +1 -1
- package/src/components/card/card.stories.tsx +11 -6
- package/src/components/card/card.tsx +1 -1
- 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 +185 -4
- package/src/components/checkbox/checkbox.tsx +1 -1
- package/src/components/color-picker/color-picker.tsx +6 -5
- package/src/components/command/command.stories.tsx +16 -6
- package/src/components/command/command.test.tsx +73 -0
- package/src/components/command/command.tsx +30 -7
- 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-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.tsx +1 -1
- package/src/components/dialog/dialog.tsx +2 -2
- package/src/components/dropdown-menu/dropdown-menu.stories.tsx +1 -1
- 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 +340 -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 +48 -6
- package/src/components/file-upload/file-upload.stories.tsx +16 -0
- package/src/components/file-upload/file-upload.test.tsx +7 -2
- package/src/components/file-upload/file-upload.tsx +15 -4
- package/src/components/form/form.tsx +5 -2
- package/src/components/icon-button/icon-button.test.tsx +1 -1
- package/src/components/icon-button/icon-button.tsx +2 -2
- package/src/components/input/input.stories.tsx +121 -0
- package/src/components/input/input.test.tsx +13 -0
- package/src/components/input/input.tsx +2 -2
- package/src/components/input-group/input-group.stories.tsx +74 -0
- package/src/components/input-group/input-group.tsx +13 -4
- package/src/components/input-otp/input-otp.tsx +5 -1
- 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/link-preview/link-preview.tsx +1 -1
- package/src/components/locale-provider/index.ts +6 -1
- 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 +501 -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.tsx +8 -38
- package/src/components/mention-input/mention-value.ts +8 -2
- 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 +1 -1
- package/src/components/nav-notifications/nav-notifications.stories.tsx +110 -0
- package/src/components/nav-notifications/nav-notifications.tsx +57 -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 +189 -12
- 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 +90 -0
- package/src/components/pagination/pagination.tsx +9 -5
- package/src/components/radio-group/radio-group.tsx +1 -1
- package/src/components/rating/rating.tsx +1 -1
- package/src/components/resizable/resizable.stories.tsx +114 -2
- package/src/components/resizable/resizable.tsx +1 -1
- 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 +46 -3
- 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.stories.tsx +566 -0
- package/src/components/schema-form/schema-form.test.tsx +900 -0
- package/src/components/schema-form/schema-form.tsx +1545 -0
- package/src/components/section-header/section-header.stories.tsx +37 -0
- package/src/components/section-header/section-header.tsx +12 -1
- package/src/components/select/select.tsx +6 -2
- package/src/components/sheet/sheet.tsx +1 -1
- 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 +273 -0
- package/src/components/side-dock/side-dock.tsx +502 -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 +36 -1
- package/src/components/sidebar/sidebar.tsx +245 -33
- 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/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 +96 -17
- package/src/components/status-badge/status-badge.stories.tsx +8 -0
- package/src/components/switch/switch.tsx +1 -1
- package/src/components/table/table.stories.tsx +62 -0
- package/src/components/table/table.test.tsx +142 -1
- package/src/components/table/table.tsx +60 -1
- package/src/components/tabs/tabs.tsx +8 -2
- package/src/components/tag-input/tag-input.tsx +3 -2
- package/src/components/team-switcher/team-switcher.tsx +34 -5
- package/src/components/textarea/textarea.tsx +1 -1
- package/src/components/theme-switcher/theme-switcher.tsx +6 -2
- 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 +11 -1
- package/src/components/toggle/toggle.tsx +1 -1
- package/src/components/toolbar/toolbar.stories.tsx +10 -2
- package/src/components/tree/tree.stories.tsx +2 -2
- package/src/components/tree/tree.tsx +14 -9
- package/src/components/typography/prose.tsx +1 -1
- 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.test.tsx +1 -1
- package/src/components/wizard/wizard.tsx +1 -1
- 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 +75 -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/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 +92 -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/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-mobile.ts +12 -6
- 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,6 +2,7 @@ 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">) {
|
|
7
8
|
return (
|
|
@@ -40,30 +41,33 @@ export function PaginationLink({ className, isActive, ...props }: PaginationLink
|
|
|
40
41
|
);
|
|
41
42
|
}
|
|
42
43
|
export function PaginationPrevious({ className, ...props }: ComponentProps<"a">) {
|
|
44
|
+
const { t } = useLocale();
|
|
43
45
|
return (
|
|
44
46
|
<a
|
|
45
|
-
aria-label="
|
|
47
|
+
aria-label={t("ui.pagination.previous")}
|
|
46
48
|
className={cn(buttonVariants({ variant: "ghost" }), "gap-1 ps-2.5 cursor-pointer", className)}
|
|
47
49
|
{...props}
|
|
48
50
|
>
|
|
49
51
|
<ChevronLeft className="size-4" data-rtl-flip />
|
|
50
|
-
<span>
|
|
52
|
+
<span>{t("previous")}</span>
|
|
51
53
|
</a>
|
|
52
54
|
);
|
|
53
55
|
}
|
|
54
56
|
export function PaginationNext({ className, ...props }: ComponentProps<"a">) {
|
|
57
|
+
const { t } = useLocale();
|
|
55
58
|
return (
|
|
56
59
|
<a
|
|
57
|
-
aria-label="
|
|
60
|
+
aria-label={t("ui.pagination.next")}
|
|
58
61
|
className={cn(buttonVariants({ variant: "ghost" }), "gap-1 pe-2.5 cursor-pointer", className)}
|
|
59
62
|
{...props}
|
|
60
63
|
>
|
|
61
|
-
<span>
|
|
64
|
+
<span>{t("next")}</span>
|
|
62
65
|
<ChevronRight className="size-4" data-rtl-flip />
|
|
63
66
|
</a>
|
|
64
67
|
);
|
|
65
68
|
}
|
|
66
69
|
export function PaginationEllipsis({ className, ...props }: ComponentProps<"span">) {
|
|
70
|
+
const { t } = useLocale();
|
|
67
71
|
return (
|
|
68
72
|
<span
|
|
69
73
|
aria-hidden
|
|
@@ -71,7 +75,7 @@ export function PaginationEllipsis({ className, ...props }: ComponentProps<"span
|
|
|
71
75
|
{...props}
|
|
72
76
|
>
|
|
73
77
|
<MoreHorizontal className="size-4" />
|
|
74
|
-
<span className="sr-only">
|
|
78
|
+
<span className="sr-only">{t("ui.pagination.morePages")}</span>
|
|
75
79
|
</span>
|
|
76
80
|
);
|
|
77
81
|
}
|
|
@@ -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
|
)}
|
|
@@ -187,7 +187,7 @@ export const Rating = forwardRef<HTMLDivElement, RatingProps>(function Rating(
|
|
|
187
187
|
aria-checked={checked}
|
|
188
188
|
aria-label={`${index} ${index === 1 ? "star" : "stars"}`}
|
|
189
189
|
tabIndex={i === tabStopIndex ? 0 : -1}
|
|
190
|
-
className="rounded-sm focus-
|
|
190
|
+
className="rounded-sm focus-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background"
|
|
191
191
|
onClick={(e) =>
|
|
192
192
|
commit(valueFromPointer(e as unknown as PointerEvent<HTMLButtonElement>, index))
|
|
193
193
|
}
|
|
@@ -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
|
};
|
|
@@ -24,7 +24,7 @@ export function ResizableHandle({
|
|
|
24
24
|
return (
|
|
25
25
|
<ResizablePrimitive.PanelResizeHandle
|
|
26
26
|
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-
|
|
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-ring",
|
|
28
28
|
"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
29
|
className,
|
|
30
30
|
)}
|
|
@@ -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`
|
|
@@ -566,7 +609,7 @@ function BranchToggle({ branchName, onToggle }: { branchName: string; onToggle:
|
|
|
566
609
|
"flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground",
|
|
567
610
|
"transition-colors duration-fast ease-standard motion-reduce:transition-none",
|
|
568
611
|
"hover:bg-accent hover:text-accent-foreground",
|
|
569
|
-
"focus-
|
|
612
|
+
"focus-ring",
|
|
570
613
|
)}
|
|
571
614
|
>
|
|
572
615
|
<BranchChevron expanded />
|
|
@@ -615,7 +658,7 @@ function CollapsedBranchRow({
|
|
|
615
658
|
className={cn(
|
|
616
659
|
"flex h-full w-full min-w-0 items-center gap-2 px-2 text-start",
|
|
617
660
|
"transition-colors duration-fast ease-standard motion-reduce:transition-none",
|
|
618
|
-
"rounded focus-
|
|
661
|
+
"rounded focus-ring",
|
|
619
662
|
"hover:bg-accent hover:text-accent-foreground",
|
|
620
663
|
)}
|
|
621
664
|
>
|
|
@@ -695,7 +738,7 @@ function RevisionRow({ revision, isMerge, density, indent, collapseToggle }: Rev
|
|
|
695
738
|
? "flex h-full min-w-0 flex-1 items-center gap-2 px-2 text-start"
|
|
696
739
|
: "flex h-full w-full min-w-0 items-center gap-2 px-2 text-start",
|
|
697
740
|
"transition-colors duration-fast ease-standard motion-reduce:transition-none",
|
|
698
|
-
"rounded focus-
|
|
741
|
+
"rounded focus-ring",
|
|
699
742
|
"hover:bg-accent hover:text-accent-foreground",
|
|
700
743
|
isSelected && "border-s-2 border-s-primary bg-accent text-accent-foreground",
|
|
701
744
|
)}
|