@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.
Files changed (223) hide show
  1. package/README.md +10 -8
  2. package/dist/chunk-JKPPTL63.js +48 -0
  3. package/dist/chunk-JKPPTL63.js.map +1 -0
  4. package/dist/form.d.ts +28 -0
  5. package/dist/form.js +123 -0
  6. package/dist/form.js.map +1 -0
  7. package/dist/index.d.ts +2485 -167
  8. package/dist/index.js +10716 -2706
  9. package/dist/index.js.map +1 -1
  10. package/package.json +21 -7
  11. package/src/_peer-deps.test.ts +59 -0
  12. package/src/blocks-comparison-table.stories.tsx +2 -2
  13. package/src/components/accordion/accordion.tsx +1 -1
  14. package/src/components/advanced-group/advanced-group.tsx +1 -1
  15. package/src/components/alert/alert.tsx +4 -1
  16. package/src/components/app-shell/app-shell.stories.tsx +44 -2
  17. package/src/components/app-shell/app-shell.tsx +35 -2
  18. package/src/components/app-sidebar/app-sidebar.stories.tsx +9 -1
  19. package/src/components/attribution-panel/attribution-panel.tsx +1 -1
  20. package/src/components/attribution-panel/attributions.generated.ts +81 -77
  21. package/src/components/badge/badge.stories.tsx +8 -0
  22. package/src/components/badge/badge.tsx +1 -1
  23. package/src/components/bento-grid/bento-grid.test.tsx +1 -1
  24. package/src/components/bento-grid/bento-grid.tsx +1 -1
  25. package/src/components/breadcrumb/breadcrumb.stories.tsx +38 -0
  26. package/src/components/breadcrumb/breadcrumb.test.tsx +24 -1
  27. package/src/components/breadcrumb/breadcrumb.tsx +7 -2
  28. package/src/components/button/button.stories.tsx +116 -8
  29. package/src/components/button/button.tsx +1 -1
  30. package/src/components/card/card.stories.tsx +11 -6
  31. package/src/components/card/card.tsx +1 -1
  32. package/src/components/change-review/change-review.stories.tsx +145 -1
  33. package/src/components/change-review/change-review.test.tsx +143 -0
  34. package/src/components/change-review/change-review.tsx +185 -4
  35. package/src/components/checkbox/checkbox.tsx +1 -1
  36. package/src/components/color-picker/color-picker.tsx +6 -5
  37. package/src/components/command/command.stories.tsx +16 -6
  38. package/src/components/command/command.test.tsx +73 -0
  39. package/src/components/command/command.tsx +30 -7
  40. package/src/components/command/index.ts +1 -0
  41. package/src/components/command-trigger/command-trigger.stories.tsx +39 -0
  42. package/src/components/command-trigger/command-trigger.test.tsx +18 -0
  43. package/src/components/command-trigger/command-trigger.tsx +70 -0
  44. package/src/components/command-trigger/index.ts +1 -0
  45. package/src/components/context-rail/context-rail.stories.tsx +285 -0
  46. package/src/components/context-rail/context-rail.test.tsx +263 -0
  47. package/src/components/context-rail/context-rail.tsx +678 -0
  48. package/src/components/context-rail/index.ts +1 -0
  49. package/src/components/copyable-value/copyable-value.tsx +1 -1
  50. package/src/components/dialog/dialog.tsx +2 -2
  51. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +1 -1
  52. package/src/components/empty-state/empty-state.test.tsx +26 -0
  53. package/src/components/empty-state/empty-state.tsx +6 -1
  54. package/src/components/expand-dialog/expand-dialog.tsx +1 -2
  55. package/src/components/field/field-context.ts +59 -0
  56. package/src/components/field/field.stories.tsx +142 -0
  57. package/src/components/field/field.test.tsx +415 -0
  58. package/src/components/field/field.tsx +340 -0
  59. package/src/components/field/index.ts +9 -0
  60. package/src/components/field-row/field-row.stories.tsx +85 -1
  61. package/src/components/field-row/field-row.test.tsx +145 -0
  62. package/src/components/field-row/field-row.tsx +48 -6
  63. package/src/components/file-upload/file-upload.stories.tsx +16 -0
  64. package/src/components/file-upload/file-upload.test.tsx +7 -2
  65. package/src/components/file-upload/file-upload.tsx +15 -4
  66. package/src/components/form/form.tsx +5 -2
  67. package/src/components/icon-button/icon-button.test.tsx +1 -1
  68. package/src/components/icon-button/icon-button.tsx +2 -2
  69. package/src/components/input/input.stories.tsx +121 -0
  70. package/src/components/input/input.test.tsx +13 -0
  71. package/src/components/input/input.tsx +2 -2
  72. package/src/components/input-group/input-group.stories.tsx +74 -0
  73. package/src/components/input-group/input-group.tsx +13 -4
  74. package/src/components/input-otp/input-otp.tsx +5 -1
  75. package/src/components/keyboard-shortcuts/index.ts +6 -0
  76. package/src/components/keyboard-shortcuts/keyboard-shortcuts.stories.tsx +177 -0
  77. package/src/components/keyboard-shortcuts/keyboard-shortcuts.test.tsx +73 -0
  78. package/src/components/keyboard-shortcuts/keyboard-shortcuts.tsx +203 -0
  79. package/src/components/link-preview/link-preview.tsx +1 -1
  80. package/src/components/locale-provider/index.ts +6 -1
  81. package/src/components/locale-provider/locale-provider.stories.tsx +89 -1
  82. package/src/components/locale-provider/locale-provider.test.tsx +257 -1
  83. package/src/components/locale-provider/locale-provider.tsx +63 -8
  84. package/src/components/locale-provider/messages.ts +501 -2
  85. package/src/components/match-highlight/match-highlight.stories.tsx +1 -1
  86. package/src/components/mention-input/mention-input-mirror.stories.tsx +9 -1
  87. package/src/components/mention-input/mention-input.tsx +8 -38
  88. package/src/components/mention-input/mention-value.ts +8 -2
  89. package/src/components/metric-card/metric-card.stories.tsx +5 -3
  90. package/src/components/metric-card/metric-card.tsx +21 -2
  91. package/src/components/model-picker/model-picker.stories.tsx +63 -3
  92. package/src/components/model-picker/model-picker.test.tsx +63 -0
  93. package/src/components/model-picker/model-picker.tsx +62 -36
  94. package/src/components/nav-main/nav-main.tsx +1 -1
  95. package/src/components/nav-notifications/nav-notifications.stories.tsx +110 -0
  96. package/src/components/nav-notifications/nav-notifications.tsx +57 -5
  97. package/src/components/navigation-menu/navigation-menu.stories.tsx +22 -0
  98. package/src/components/navigation-menu/navigation-menu.test.tsx +174 -0
  99. package/src/components/navigation-menu/navigation-menu.tsx +189 -12
  100. package/src/components/page-shell/page-shell.stories.tsx +163 -0
  101. package/src/components/page-shell/page-shell.test.tsx +229 -0
  102. package/src/components/page-shell/page-shell.tsx +108 -25
  103. package/src/components/pagination/pagination.test.tsx +90 -0
  104. package/src/components/pagination/pagination.tsx +9 -5
  105. package/src/components/radio-group/radio-group.tsx +1 -1
  106. package/src/components/rating/rating.tsx +1 -1
  107. package/src/components/resizable/resizable.stories.tsx +114 -2
  108. package/src/components/resizable/resizable.tsx +1 -1
  109. package/src/components/revision-timeline/revision-timeline.stories.tsx +8 -1
  110. package/src/components/revision-timeline/revision-timeline.test.tsx +26 -0
  111. package/src/components/revision-timeline/revision-timeline.tsx +46 -3
  112. package/src/components/schema-form/from-json-schema.test.ts +390 -0
  113. package/src/components/schema-form/from-json-schema.ts +375 -0
  114. package/src/components/schema-form/index.ts +81 -0
  115. package/src/components/schema-form/schema-form-spec.ts +698 -0
  116. package/src/components/schema-form/schema-form.stories.tsx +566 -0
  117. package/src/components/schema-form/schema-form.test.tsx +900 -0
  118. package/src/components/schema-form/schema-form.tsx +1545 -0
  119. package/src/components/section-header/section-header.stories.tsx +37 -0
  120. package/src/components/section-header/section-header.tsx +12 -1
  121. package/src/components/select/select.tsx +6 -2
  122. package/src/components/sheet/sheet.tsx +1 -1
  123. package/src/components/side-dock/index.ts +1 -0
  124. package/src/components/side-dock/side-dock.stories.tsx +137 -0
  125. package/src/components/side-dock/side-dock.test.tsx +273 -0
  126. package/src/components/side-dock/side-dock.tsx +502 -0
  127. package/src/components/sidebar/index.ts +1 -0
  128. package/src/components/sidebar/sidebar-frame.test.tsx +162 -0
  129. package/src/components/sidebar/sidebar.stories.tsx +421 -1
  130. package/src/components/sidebar/sidebar.test.tsx +36 -1
  131. package/src/components/sidebar/sidebar.tsx +245 -33
  132. package/src/components/skip-link/index.ts +1 -0
  133. package/src/components/skip-link/skip-link.stories.tsx +106 -0
  134. package/src/components/skip-link/skip-link.test.tsx +20 -0
  135. package/src/components/skip-link/skip-link.tsx +34 -0
  136. package/src/components/slider/slider.tsx +1 -1
  137. package/src/components/split-panel/index.ts +6 -1
  138. package/src/components/split-panel/split-panel.stories.tsx +18 -2
  139. package/src/components/split-panel/split-panel.test.tsx +20 -0
  140. package/src/components/split-panel/split-panel.tsx +28 -8
  141. package/src/components/state-panel/state-panel.stories.tsx +52 -0
  142. package/src/components/state-panel/state-panel.test.tsx +125 -0
  143. package/src/components/state-panel/state-panel.tsx +96 -17
  144. package/src/components/status-badge/status-badge.stories.tsx +8 -0
  145. package/src/components/switch/switch.tsx +1 -1
  146. package/src/components/table/table.stories.tsx +62 -0
  147. package/src/components/table/table.test.tsx +142 -1
  148. package/src/components/table/table.tsx +60 -1
  149. package/src/components/tabs/tabs.tsx +8 -2
  150. package/src/components/tag-input/tag-input.tsx +3 -2
  151. package/src/components/team-switcher/team-switcher.tsx +34 -5
  152. package/src/components/textarea/textarea.tsx +1 -1
  153. package/src/components/theme-switcher/theme-switcher.tsx +6 -2
  154. package/src/components/theme-switcher/use-theme-transition.ts +2 -2
  155. package/src/components/timeline/timeline.stories.tsx +27 -0
  156. package/src/components/timeline/timeline.tsx +11 -1
  157. package/src/components/toggle/toggle.tsx +1 -1
  158. package/src/components/toolbar/toolbar.stories.tsx +10 -2
  159. package/src/components/tree/tree.stories.tsx +2 -2
  160. package/src/components/tree/tree.tsx +14 -9
  161. package/src/components/typography/prose.tsx +1 -1
  162. package/src/components/typography/typography.stories.tsx +228 -0
  163. package/src/components/view-toolbar/view-toolbar.stories.tsx +41 -1
  164. package/src/components/view-toolbar/view-toolbar.test.tsx +33 -0
  165. package/src/components/view-toolbar/view-toolbar.tsx +37 -4
  166. package/src/components/virtual-select/virtual-select.test.tsx +1 -1
  167. package/src/components/wizard/wizard.tsx +1 -1
  168. package/src/components/workspace-picker/index.ts +3 -0
  169. package/src/components/workspace-picker/workspace-picker-state.ts +50 -0
  170. package/src/components/workspace-picker/workspace-picker.stories.tsx +126 -0
  171. package/src/components/workspace-picker/workspace-picker.test.tsx +207 -0
  172. package/src/components/workspace-picker/workspace-picker.tsx +172 -0
  173. package/src/illustrations/empty-list-illustration.tsx +26 -0
  174. package/src/illustrations/error-illustration.tsx +60 -0
  175. package/src/illustrations/first-run-illustration.tsx +27 -0
  176. package/src/illustrations/illustration-base.tsx +108 -0
  177. package/src/illustrations/illustrations.stories.tsx +94 -0
  178. package/src/illustrations/illustrations.test.tsx +98 -0
  179. package/src/illustrations/index.ts +16 -0
  180. package/src/illustrations/no-access-illustration.tsx +45 -0
  181. package/src/illustrations/no-results-illustration.tsx +46 -0
  182. package/src/illustrations/offline-illustration.tsx +37 -0
  183. package/src/illustrations/success-illustration.tsx +40 -0
  184. package/src/index.ts +75 -2
  185. package/src/lib/agent-event-model.test.ts +12 -0
  186. package/src/lib/agent-event-model.ts +42 -0
  187. package/src/lib/approval-option.test.ts +17 -0
  188. package/src/lib/approval-option.ts +53 -0
  189. package/src/lib/check-result.ts +33 -0
  190. package/src/lib/diff-line.test.ts +36 -0
  191. package/src/lib/diff-line.ts +63 -0
  192. package/src/lib/diff-rows.test.ts +76 -0
  193. package/src/lib/diff-rows.ts +92 -0
  194. package/src/lib/format-duration.test.ts +22 -0
  195. package/src/lib/format-duration.ts +30 -0
  196. package/src/lib/has-renderable-content.test.ts +49 -0
  197. package/src/lib/has-renderable-content.ts +32 -0
  198. package/src/lib/operating-mode.test.ts +36 -0
  199. package/src/lib/operating-mode.ts +81 -0
  200. package/src/lib/optional-peer.ts +59 -0
  201. package/src/lib/session-launch.ts +37 -0
  202. package/src/lib/slash-command.test.ts +44 -0
  203. package/src/lib/slash-command.ts +45 -0
  204. package/src/lib/trigger-query.test.ts +94 -0
  205. package/src/lib/trigger-query.ts +88 -0
  206. package/src/lib/use-mobile.ts +12 -6
  207. package/src/motion.stories.tsx +6 -0
  208. package/src/templates-object-detail-hub.stories.tsx +2 -2
  209. package/src/templates-screen-states.stories.tsx +5 -5
  210. package/src/templates-settings.stories.tsx +1 -1
  211. package/src/blocks/sidebar-02/app-sidebar.tsx +0 -160
  212. package/src/blocks/sidebar-02/logo.tsx +0 -18
  213. package/src/blocks/sidebar-02/nav-main.tsx +0 -13
  214. package/src/blocks/sidebar-02/nav-notifications.tsx +0 -7
  215. package/src/blocks/sidebar-02/sidebar-02.stories.tsx +0 -23
  216. package/src/blocks/sidebar-02/team-switcher.tsx +0 -7
  217. package/src/blocks/sidebar-04/app-sidebar.tsx +0 -236
  218. package/src/blocks/sidebar-04/mail-context.tsx +0 -29
  219. package/src/blocks/sidebar-04/nav-user.tsx +0 -6
  220. package/src/blocks/sidebar-04/sidebar-04.stories.tsx +0 -150
  221. package/src/blocks/sidebar-05/app-sidebar.tsx +0 -375
  222. package/src/blocks/sidebar-05/sidebar-05.stories.tsx +0 -18
  223. 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="Go to previous page"
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>Previous</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="Go to next page"
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>Next</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">More pages</span>
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-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
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-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background"
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
- parameters: { layout: "padded" },
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-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
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-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
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-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
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-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
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
  )}