@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.
Files changed (339) hide show
  1. package/README.md +10 -8
  2. package/dist/chunk-FCH4ZN6G.js +57 -0
  3. package/dist/chunk-FCH4ZN6G.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 +2597 -188
  8. package/dist/index.js +12855 -3578
  9. package/dist/index.js.map +1 -1
  10. package/dist/lib/cn.js +10 -1
  11. package/dist/lib/cn.js.map +1 -1
  12. package/package.json +21 -7
  13. package/src/__contract__/attribution-panel.contract.test.tsx +49 -0
  14. package/src/__contract__/badge.contract.test.tsx +49 -0
  15. package/src/__contract__/bounded-number.contract.test.tsx +49 -0
  16. package/src/__contract__/button.contract.test.tsx +49 -0
  17. package/src/__contract__/color-picker.contract.test.tsx +49 -0
  18. package/src/__contract__/combobox.contract.test.tsx +49 -0
  19. package/src/__contract__/command-trigger.contract.test.tsx +49 -0
  20. package/src/__contract__/copyable-value.contract.test.tsx +49 -0
  21. package/src/__contract__/date-picker.contract.test.tsx +49 -0
  22. package/src/__contract__/date-range-picker.contract.test.tsx +49 -0
  23. package/src/__contract__/empty-state.contract.test.tsx +49 -0
  24. package/src/__contract__/error-state.contract.test.tsx +49 -0
  25. package/src/__contract__/icon-button.contract.test.tsx +49 -0
  26. package/src/__contract__/input.contract.test.tsx +49 -0
  27. package/src/__contract__/key-value-editor.contract.test.tsx +49 -0
  28. package/src/__contract__/list-editor.contract.test.tsx +49 -0
  29. package/src/__contract__/loading-state.contract.test.tsx +49 -0
  30. package/src/__contract__/metric-card.contract.test.tsx +49 -0
  31. package/src/__contract__/model-picker.contract.test.tsx +49 -0
  32. package/src/__contract__/nav-notifications.contract.test.tsx +49 -0
  33. package/src/__contract__/number-input.contract.test.tsx +49 -0
  34. package/src/__contract__/rating.contract.test.tsx +49 -0
  35. package/src/__contract__/schema-form.contract.test.tsx +49 -0
  36. package/src/__contract__/section-header.contract.test.tsx +49 -0
  37. package/src/__contract__/segmented-field.contract.test.tsx +49 -0
  38. package/src/__contract__/slider-number.contract.test.tsx +49 -0
  39. package/src/__contract__/status-badge.contract.test.tsx +49 -0
  40. package/src/__contract__/tag-input.contract.test.tsx +49 -0
  41. package/src/__contract__/text.contract.test.tsx +49 -0
  42. package/src/__contract__/textarea.contract.test.tsx +49 -0
  43. package/src/__contract__/timeline.contract.test.tsx +49 -0
  44. package/src/__contract__/toggle.contract.test.tsx +49 -0
  45. package/src/__contract__/transfer.contract.test.tsx +49 -0
  46. package/src/__contract__/tree-select.contract.test.tsx +49 -0
  47. package/src/__contract__/tree.contract.test.tsx +49 -0
  48. package/src/__contract__/virtual-select.contract.test.tsx +49 -0
  49. package/src/__contract__/workspace-picker.contract.test.tsx +49 -0
  50. package/src/_peer-deps.test.ts +59 -0
  51. package/src/blocks-comparison-table.stories.tsx +2 -2
  52. package/src/components/accordion/accordion.tsx +13 -4
  53. package/src/components/advanced-group/advanced-group.tsx +1 -1
  54. package/src/components/alert/alert.tsx +15 -4
  55. package/src/components/alert-dialog/alert-dialog.test.tsx +32 -0
  56. package/src/components/alert-dialog/alert-dialog.tsx +28 -6
  57. package/src/components/app-shell/app-shell.stories.tsx +44 -2
  58. package/src/components/app-shell/app-shell.test.tsx +99 -0
  59. package/src/components/app-shell/app-shell.tsx +56 -4
  60. package/src/components/app-sidebar/app-sidebar.stories.tsx +9 -1
  61. package/src/components/attribution-panel/attribution-panel.tsx +3 -1
  62. package/src/components/attribution-panel/attributions.generated.ts +83 -79
  63. package/src/components/avatar/avatar.tsx +4 -1
  64. package/src/components/badge/badge.stories.tsx +8 -0
  65. package/src/components/badge/badge.test.tsx +8 -0
  66. package/src/components/badge/badge.tsx +15 -5
  67. package/src/components/bento-grid/bento-grid.test.tsx +24 -3
  68. package/src/components/bento-grid/bento-grid.tsx +11 -1
  69. package/src/components/bounded-number/bounded-number.test.tsx +1 -1
  70. package/src/components/bounded-number/bounded-number.tsx +78 -75
  71. package/src/components/breadcrumb/breadcrumb.stories.tsx +38 -0
  72. package/src/components/breadcrumb/breadcrumb.test.tsx +24 -1
  73. package/src/components/breadcrumb/breadcrumb.tsx +10 -4
  74. package/src/components/button/button.stories.tsx +116 -8
  75. package/src/components/button/button.tsx +18 -12
  76. package/src/components/button-group/button-group.tsx +1 -1
  77. package/src/components/calendar/calendar.tsx +3 -2
  78. package/src/components/card/card.stories.tsx +11 -6
  79. package/src/components/card/card.test.tsx +24 -2
  80. package/src/components/card/card.tsx +48 -7
  81. package/src/components/carousel/carousel.test.tsx +169 -0
  82. package/src/components/carousel/carousel.tsx +93 -32
  83. package/src/components/change-review/change-review.stories.tsx +145 -1
  84. package/src/components/change-review/change-review.test.tsx +143 -0
  85. package/src/components/change-review/change-review.tsx +214 -24
  86. package/src/components/checkbox/checkbox.tsx +2 -1
  87. package/src/components/collapsible-panel/use-collapsible-panel.ts +0 -3
  88. package/src/components/color-picker/color-picker.tsx +22 -15
  89. package/src/components/combobox/combobox.stories.tsx +10 -0
  90. package/src/components/combobox/combobox.test.tsx +54 -0
  91. package/src/components/combobox/combobox.tsx +25 -11
  92. package/src/components/command/command.stories.tsx +16 -6
  93. package/src/components/command/command.test.tsx +88 -0
  94. package/src/components/command/command.tsx +38 -13
  95. package/src/components/command/index.ts +1 -0
  96. package/src/components/command-trigger/command-trigger.stories.tsx +39 -0
  97. package/src/components/command-trigger/command-trigger.test.tsx +18 -0
  98. package/src/components/command-trigger/command-trigger.tsx +70 -0
  99. package/src/components/command-trigger/index.ts +1 -0
  100. package/src/components/context-menu/context-menu.tsx +16 -7
  101. package/src/components/context-rail/context-rail.stories.tsx +285 -0
  102. package/src/components/context-rail/context-rail.test.tsx +263 -0
  103. package/src/components/context-rail/context-rail.tsx +678 -0
  104. package/src/components/context-rail/index.ts +1 -0
  105. package/src/components/copyable-value/copyable-value.stories.tsx +10 -0
  106. package/src/components/copyable-value/copyable-value.test.tsx +28 -1
  107. package/src/components/copyable-value/copyable-value.tsx +13 -2
  108. package/src/components/date-picker/date-picker.stories.tsx +9 -0
  109. package/src/components/date-picker/date-picker.test.tsx +52 -0
  110. package/src/components/date-picker/date-picker.tsx +13 -15
  111. package/src/components/date-range-picker/date-range-picker.stories.tsx +9 -0
  112. package/src/components/date-range-picker/date-range-picker.test.tsx +76 -1
  113. package/src/components/date-range-picker/date-range-picker.tsx +39 -28
  114. package/src/components/descriptions/descriptions.tsx +1 -1
  115. package/src/components/dialog/dialog.test.tsx +8 -3
  116. package/src/components/dialog/dialog.tsx +14 -6
  117. package/src/components/drawer/drawer.tsx +23 -6
  118. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +1 -1
  119. package/src/components/dropdown-menu/dropdown-menu.tsx +4 -4
  120. package/src/components/empty-state/empty-state.test.tsx +26 -0
  121. package/src/components/empty-state/empty-state.tsx +6 -1
  122. package/src/components/expand-dialog/expand-dialog.tsx +1 -2
  123. package/src/components/field/field-context.ts +59 -0
  124. package/src/components/field/field.stories.tsx +142 -0
  125. package/src/components/field/field.test.tsx +415 -0
  126. package/src/components/field/field.tsx +342 -0
  127. package/src/components/field/index.ts +9 -0
  128. package/src/components/field-row/field-row.stories.tsx +85 -1
  129. package/src/components/field-row/field-row.test.tsx +145 -0
  130. package/src/components/field-row/field-row.tsx +50 -6
  131. package/src/components/file-upload/file-upload.stories.tsx +16 -0
  132. package/src/components/file-upload/file-upload.test.tsx +90 -12
  133. package/src/components/file-upload/file-upload.tsx +164 -31
  134. package/src/components/form/form.tsx +8 -3
  135. package/src/components/hover-card/hover-card.tsx +1 -0
  136. package/src/components/icon-button/icon-button.test.tsx +1 -1
  137. package/src/components/icon-button/icon-button.tsx +3 -3
  138. package/src/components/input/input.stories.tsx +121 -0
  139. package/src/components/input/input.test.tsx +13 -0
  140. package/src/components/input/input.tsx +8 -4
  141. package/src/components/input-group/input-group.stories.tsx +74 -0
  142. package/src/components/input-group/input-group.tsx +18 -9
  143. package/src/components/input-otp/input-otp.tsx +8 -2
  144. package/src/components/keyboard-shortcuts/index.ts +6 -0
  145. package/src/components/keyboard-shortcuts/keyboard-shortcuts.stories.tsx +177 -0
  146. package/src/components/keyboard-shortcuts/keyboard-shortcuts.test.tsx +73 -0
  147. package/src/components/keyboard-shortcuts/keyboard-shortcuts.tsx +203 -0
  148. package/src/components/label/label.tsx +1 -1
  149. package/src/components/link-preview/link-preview.tsx +1 -1
  150. package/src/components/list-editor/list-editor.test.tsx +24 -0
  151. package/src/components/list-editor/list-editor.tsx +126 -52
  152. package/src/components/locale-provider/index.ts +6 -1
  153. package/src/components/locale-provider/locale-keys.test.ts +134 -0
  154. package/src/components/locale-provider/locale-provider.stories.tsx +89 -1
  155. package/src/components/locale-provider/locale-provider.test.tsx +257 -1
  156. package/src/components/locale-provider/locale-provider.tsx +63 -8
  157. package/src/components/locale-provider/messages.ts +822 -2
  158. package/src/components/match-highlight/match-highlight.stories.tsx +1 -1
  159. package/src/components/mention-input/mention-input-mirror.stories.tsx +9 -1
  160. package/src/components/mention-input/mention-input.stories.tsx +3 -3
  161. package/src/components/mention-input/mention-input.test.tsx +51 -1
  162. package/src/components/mention-input/mention-input.tsx +66 -46
  163. package/src/components/mention-input/mention-value.ts +8 -2
  164. package/src/components/menubar/menubar.tsx +16 -6
  165. package/src/components/metric-card/metric-card.stories.tsx +5 -3
  166. package/src/components/metric-card/metric-card.tsx +21 -2
  167. package/src/components/model-picker/model-picker.stories.tsx +63 -3
  168. package/src/components/model-picker/model-picker.test.tsx +63 -0
  169. package/src/components/model-picker/model-picker.tsx +62 -36
  170. package/src/components/nav-main/nav-main.tsx +8 -4
  171. package/src/components/nav-notifications/nav-notifications.stories.tsx +110 -0
  172. package/src/components/nav-notifications/nav-notifications.test.tsx +11 -0
  173. package/src/components/nav-notifications/nav-notifications.tsx +67 -9
  174. package/src/components/nav-user/nav-user.tsx +5 -5
  175. package/src/components/navigation-menu/navigation-menu.stories.tsx +22 -0
  176. package/src/components/navigation-menu/navigation-menu.test.tsx +174 -0
  177. package/src/components/navigation-menu/navigation-menu.tsx +204 -15
  178. package/src/components/number-input/number-input.stories.tsx +7 -3
  179. package/src/components/number-input/number-input.test.tsx +47 -0
  180. package/src/components/number-input/number-input.tsx +65 -24
  181. package/src/components/page-shell/page-shell.stories.tsx +163 -0
  182. package/src/components/page-shell/page-shell.test.tsx +229 -0
  183. package/src/components/page-shell/page-shell.tsx +108 -25
  184. package/src/components/pagination/pagination.test.tsx +109 -0
  185. package/src/components/pagination/pagination.tsx +27 -19
  186. package/src/components/popover/popover.tsx +1 -0
  187. package/src/components/progress/progress.tsx +2 -0
  188. package/src/components/radio-group/radio-group.tsx +1 -1
  189. package/src/components/rating/rating.tsx +3 -1
  190. package/src/components/resizable/resizable.stories.tsx +114 -2
  191. package/src/components/resizable/resizable.test.tsx +76 -0
  192. package/src/components/resizable/resizable.tsx +32 -6
  193. package/src/components/revision-timeline/revision-timeline.stories.tsx +8 -1
  194. package/src/components/revision-timeline/revision-timeline.test.tsx +26 -0
  195. package/src/components/revision-timeline/revision-timeline.tsx +53 -5
  196. package/src/components/schema-form/from-json-schema.test.ts +390 -0
  197. package/src/components/schema-form/from-json-schema.ts +375 -0
  198. package/src/components/schema-form/index.ts +81 -0
  199. package/src/components/schema-form/schema-form-spec.ts +698 -0
  200. package/src/components/schema-form/schema-form-store.ts +506 -0
  201. package/src/components/schema-form/schema-form.stories.tsx +566 -0
  202. package/src/components/schema-form/schema-form.test.tsx +1021 -0
  203. package/src/components/schema-form/schema-form.tsx +1381 -0
  204. package/src/components/section-header/section-header.stories.tsx +37 -0
  205. package/src/components/section-header/section-header.tsx +14 -3
  206. package/src/components/select/select.tsx +13 -7
  207. package/src/components/separator/separator.tsx +1 -0
  208. package/src/components/sheet/sheet.test.tsx +39 -0
  209. package/src/components/sheet/sheet.tsx +40 -9
  210. package/src/components/side-dock/index.ts +1 -0
  211. package/src/components/side-dock/side-dock.stories.tsx +137 -0
  212. package/src/components/side-dock/side-dock.test.tsx +299 -0
  213. package/src/components/side-dock/side-dock.tsx +515 -0
  214. package/src/components/sidebar/index.ts +1 -0
  215. package/src/components/sidebar/sidebar-frame.test.tsx +162 -0
  216. package/src/components/sidebar/sidebar.stories.tsx +421 -1
  217. package/src/components/sidebar/sidebar.test.tsx +71 -2
  218. package/src/components/sidebar/sidebar.tsx +264 -38
  219. package/src/components/skeleton/skeleton.tsx +1 -0
  220. package/src/components/skip-link/index.ts +1 -0
  221. package/src/components/skip-link/skip-link.stories.tsx +106 -0
  222. package/src/components/skip-link/skip-link.test.tsx +20 -0
  223. package/src/components/skip-link/skip-link.tsx +34 -0
  224. package/src/components/slider/slider.tsx +1 -1
  225. package/src/components/slider-number/slider-number.stories.tsx +4 -1
  226. package/src/components/slider-number/slider-number.test.tsx +1 -1
  227. package/src/components/sonner/sonner.tsx +2 -0
  228. package/src/components/spinner/spinner.test.tsx +18 -0
  229. package/src/components/spinner/spinner.tsx +14 -4
  230. package/src/components/split-panel/index.ts +6 -1
  231. package/src/components/split-panel/split-panel.stories.tsx +18 -2
  232. package/src/components/split-panel/split-panel.test.tsx +20 -0
  233. package/src/components/split-panel/split-panel.tsx +28 -8
  234. package/src/components/state-panel/state-panel.stories.tsx +52 -0
  235. package/src/components/state-panel/state-panel.test.tsx +125 -0
  236. package/src/components/state-panel/state-panel.tsx +110 -24
  237. package/src/components/status-badge/status-badge.stories.tsx +8 -0
  238. package/src/components/switch/switch.tsx +3 -1
  239. package/src/components/table/table.stories.tsx +64 -0
  240. package/src/components/table/table.test.tsx +149 -1
  241. package/src/components/table/table.tsx +71 -2
  242. package/src/components/tabs/index.ts +10 -1
  243. package/src/components/tabs/tabs.stories.tsx +48 -0
  244. package/src/components/tabs/tabs.test.tsx +67 -0
  245. package/src/components/tabs/tabs.tsx +176 -72
  246. package/src/components/tag-input/tag-input.tsx +6 -3
  247. package/src/components/team-switcher/team-switcher.tsx +35 -6
  248. package/src/components/textarea/textarea.tsx +2 -2
  249. package/src/components/theme-switcher/theme-switcher.stories.tsx +76 -0
  250. package/src/components/theme-switcher/theme-switcher.test.tsx +182 -0
  251. package/src/components/theme-switcher/theme-switcher.tsx +140 -12
  252. package/src/components/theme-switcher/use-theme-transition.ts +2 -2
  253. package/src/components/timeline/timeline.stories.tsx +27 -0
  254. package/src/components/timeline/timeline.tsx +10 -3
  255. package/src/components/toggle/toggle.tsx +7 -2
  256. package/src/components/toggle-group/toggle-group.tsx +2 -0
  257. package/src/components/toolbar/toolbar.stories.tsx +10 -2
  258. package/src/components/tooltip/tooltip.tsx +2 -1
  259. package/src/components/top-nav/top-nav.tsx +1 -1
  260. package/src/components/transfer/transfer.tsx +35 -30
  261. package/src/components/tree/tree.stories.tsx +2 -2
  262. package/src/components/tree/tree.test.tsx +41 -0
  263. package/src/components/tree/tree.tsx +91 -31
  264. package/src/components/tree/use-tree-keyboard.ts +37 -14
  265. package/src/components/tree-select/tree-select.stories.tsx +9 -0
  266. package/src/components/tree-select/tree-select.test.tsx +5 -0
  267. package/src/components/tree-select/tree-select.tsx +11 -14
  268. package/src/components/typography/prose.test.tsx +3 -2
  269. package/src/components/typography/prose.tsx +3 -3
  270. package/src/components/typography/typography.stories.tsx +228 -0
  271. package/src/components/view-toolbar/view-toolbar.stories.tsx +41 -1
  272. package/src/components/view-toolbar/view-toolbar.test.tsx +33 -0
  273. package/src/components/view-toolbar/view-toolbar.tsx +37 -4
  274. package/src/components/virtual-select/virtual-select.stories.tsx +9 -0
  275. package/src/components/virtual-select/virtual-select.test.tsx +29 -3
  276. package/src/components/virtual-select/virtual-select.tsx +56 -18
  277. package/src/components/wizard/wizard.test.tsx +9 -0
  278. package/src/components/wizard/wizard.tsx +13 -5
  279. package/src/components/workspace-picker/index.ts +3 -0
  280. package/src/components/workspace-picker/workspace-picker-state.ts +50 -0
  281. package/src/components/workspace-picker/workspace-picker.stories.tsx +126 -0
  282. package/src/components/workspace-picker/workspace-picker.test.tsx +207 -0
  283. package/src/components/workspace-picker/workspace-picker.tsx +172 -0
  284. package/src/illustrations/empty-list-illustration.tsx +26 -0
  285. package/src/illustrations/error-illustration.tsx +60 -0
  286. package/src/illustrations/first-run-illustration.tsx +27 -0
  287. package/src/illustrations/illustration-base.tsx +108 -0
  288. package/src/illustrations/illustrations.stories.tsx +94 -0
  289. package/src/illustrations/illustrations.test.tsx +98 -0
  290. package/src/illustrations/index.ts +16 -0
  291. package/src/illustrations/no-access-illustration.tsx +45 -0
  292. package/src/illustrations/no-results-illustration.tsx +46 -0
  293. package/src/illustrations/offline-illustration.tsx +37 -0
  294. package/src/illustrations/success-illustration.tsx +40 -0
  295. package/src/index.ts +80 -2
  296. package/src/lib/agent-event-model.test.ts +12 -0
  297. package/src/lib/agent-event-model.ts +42 -0
  298. package/src/lib/approval-option.test.ts +17 -0
  299. package/src/lib/approval-option.ts +53 -0
  300. package/src/lib/check-result.ts +33 -0
  301. package/src/lib/cn.ts +9 -0
  302. package/src/lib/csv.ts +48 -0
  303. package/src/lib/diff-line.test.ts +36 -0
  304. package/src/lib/diff-line.ts +63 -0
  305. package/src/lib/diff-rows.test.ts +76 -0
  306. package/src/lib/diff-rows.ts +94 -0
  307. package/src/lib/format-duration.test.ts +22 -0
  308. package/src/lib/format-duration.ts +30 -0
  309. package/src/lib/has-renderable-content.test.ts +49 -0
  310. package/src/lib/has-renderable-content.ts +32 -0
  311. package/src/lib/merge-refs.ts +2 -0
  312. package/src/lib/operating-mode.test.ts +36 -0
  313. package/src/lib/operating-mode.ts +81 -0
  314. package/src/lib/optional-peer.ts +59 -0
  315. package/src/lib/session-launch.ts +37 -0
  316. package/src/lib/slash-command.test.ts +44 -0
  317. package/src/lib/slash-command.ts +45 -0
  318. package/src/lib/trigger-query.test.ts +94 -0
  319. package/src/lib/trigger-query.ts +88 -0
  320. package/src/lib/use-controllable-state.ts +47 -0
  321. package/src/lib/use-mobile.test.tsx +47 -0
  322. package/src/lib/use-mobile.ts +34 -12
  323. package/src/motion.stories.tsx +6 -0
  324. package/src/templates-object-detail-hub.stories.tsx +2 -2
  325. package/src/templates-screen-states.stories.tsx +5 -5
  326. package/src/templates-settings.stories.tsx +1 -1
  327. package/src/blocks/sidebar-02/app-sidebar.tsx +0 -160
  328. package/src/blocks/sidebar-02/logo.tsx +0 -18
  329. package/src/blocks/sidebar-02/nav-main.tsx +0 -13
  330. package/src/blocks/sidebar-02/nav-notifications.tsx +0 -7
  331. package/src/blocks/sidebar-02/sidebar-02.stories.tsx +0 -23
  332. package/src/blocks/sidebar-02/team-switcher.tsx +0 -7
  333. package/src/blocks/sidebar-04/app-sidebar.tsx +0 -236
  334. package/src/blocks/sidebar-04/mail-context.tsx +0 -29
  335. package/src/blocks/sidebar-04/nav-user.tsx +0 -6
  336. package/src/blocks/sidebar-04/sidebar-04.stories.tsx +0 -150
  337. package/src/blocks/sidebar-05/app-sidebar.tsx +0 -375
  338. package/src/blocks/sidebar-05/sidebar-05.stories.tsx +0 -18
  339. package/src/blocks/sidebar-05/team-switcher.tsx +0 -6
@@ -1,4 +1,5 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, within } from "storybook/test";
2
3
  import { Button } from "../button";
3
4
  import { SectionHeader } from "./section-header";
4
5
 
@@ -18,3 +19,39 @@ export const Default: Story = {
18
19
  actions: <Button size="sm">New project</Button>,
19
20
  },
20
21
  };
22
+
23
+ /**
24
+ * `as` picks the heading LEVEL, never the size — the title renders at
25
+ * `text-title` for every value. Reach for `as="h1"` when this header titles the
26
+ * page itself; a screen whose highest heading is an `<h2>` has no document
27
+ * outline root at all (WCAG 1.3.1).
28
+ */
29
+ export const PageTitle: Story = {
30
+ args: {
31
+ eyebrow: "Workspace",
32
+ as: "h1",
33
+ title: "Projects",
34
+ description: "The page’s own title, so it owns the h1.",
35
+ },
36
+ play: async ({ canvasElement }) => {
37
+ const canvas = within(canvasElement);
38
+ // Resolve BY ROLE AND LEVEL: `getByText` would find the same node whatever
39
+ // tag it rendered as, so it could not tell an <h1> from the <h2> default.
40
+ const h1 = canvas.getByRole("heading", { level: 1, name: "Projects" });
41
+ await expect(h1.tagName).toBe("H1");
42
+ // The visual is unchanged by the level — that is the whole point of the
43
+ // prop, and the reason nobody should reach for it to get a bigger title.
44
+ await expect(h1.classList.contains("text-title")).toBe(true);
45
+ },
46
+ };
47
+
48
+ /** The default is still `<h2>`, so every existing caller is untouched. */
49
+ export const DefaultLevelIsH2: Story = {
50
+ args: { title: "Projects" },
51
+ play: async ({ canvasElement }) => {
52
+ const canvas = within(canvasElement);
53
+ const heading = canvas.getByRole("heading", { name: "Projects" });
54
+ await expect(heading.tagName).toBe("H2");
55
+ await expect(canvas.queryByRole("heading", { level: 1 })).toBeNull();
56
+ },
57
+ };
@@ -8,6 +8,16 @@ export interface SectionHeaderProps {
8
8
  actions?: ReactNode;
9
9
  /** Optional element rendered above the title (eyebrow / breadcrumbs). */
10
10
  eyebrow?: ReactNode;
11
+ /**
12
+ * The heading LEVEL the title contributes to the document outline. The
13
+ * visual (`text-title`) is identical for every value — pick the level the
14
+ * page's structure calls for, not the size you want (WCAG 1.3.1).
15
+ *
16
+ * Use `as="h1"` when this header titles the PAGE (a screen whose route it
17
+ * names); leave it at the default when it titles a section INSIDE a page
18
+ * that already has an `<h1>` elsewhere. @default "h2"
19
+ */
20
+ as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
11
21
  className?: string;
12
22
  }
13
23
 
@@ -17,18 +27,19 @@ export function SectionHeader({
17
27
  description,
18
28
  actions,
19
29
  eyebrow,
30
+ as: TitleTag = "h2",
20
31
  className,
21
32
  }: SectionHeaderProps) {
22
33
  return (
23
34
  <div className={cn("flex flex-wrap items-end justify-between gap-4", className)}>
24
35
  <div className="space-y-1">
25
36
  {eyebrow ? (
26
- <div className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
37
+ <div className="text-meta font-medium uppercase tracking-wide text-muted-foreground">
27
38
  {eyebrow}
28
39
  </div>
29
40
  ) : null}
30
- <h2 className="text-title text-foreground">{title}</h2>
31
- {description ? <p className="text-sm text-muted-foreground">{description}</p> : null}
41
+ <TitleTag className="text-title text-foreground">{title}</TitleTag>
42
+ {description ? <p className="text-body text-muted-foreground">{description}</p> : null}
32
43
  </div>
33
44
  {actions ? <div className="flex items-center gap-2">{actions}</div> : null}
34
45
  </div>
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import {
2
4
  forwardRef,
3
5
  useEffect,
@@ -121,13 +123,17 @@ export const SelectTrigger = forwardRef<
121
123
  data-size={size}
122
124
  title={title}
123
125
  className={cn(
124
- "flex w-full items-center justify-between gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm",
125
- size === "sm" ? "h-8" : "h-9",
126
- "placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1 focus:ring-offset-background",
126
+ "flex w-full items-center justify-between gap-2 rounded-control border border-input bg-input-background px-3 py-2 text-body shadow-input",
127
+ size === "sm" ? "h-control-sm" : "h-control",
128
+ "placeholder:text-muted-foreground focus-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
127
129
  // No `disabled:cursor-*` override — disabled controls keep the arrow
128
130
  // automatically (interaction-guidelines.md), matching Combobox's
129
131
  // Button-based trigger (`disabled:pointer-events-none`) (#343).
130
- "disabled:opacity-50 disabled:border-border [&>span]:line-clamp-1",
132
+ // `truncate`, not `line-clamp-1`: a clamp only prints its ellipsis when it has
133
+ // dropped a LINE, so a single-line value too wide for a narrow trigger was cut
134
+ // mid-word with no ellipsis at all ("Occurren"). `autoTitle` above puts the full
135
+ // text in the tooltip, so the ellipsis is the visible half of that pair.
136
+ "disabled:opacity-50 disabled:border-border [&>span]:truncate",
131
137
  className,
132
138
  )}
133
139
  {...props}
@@ -150,7 +156,7 @@ export const SelectContent = forwardRef<
150
156
  ref={ref}
151
157
  position={position}
152
158
  className={cn(
153
- "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md bg-popover text-popover-foreground shadow-ring-md",
159
+ "relative z-50 max-h-96 min-w-[12rem] overflow-hidden rounded-md bg-popover text-popover-foreground shadow-ring-md",
154
160
  "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95",
155
161
  "data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
156
162
  position === "popper" &&
@@ -186,7 +192,7 @@ export const SelectLabel = forwardRef<
186
192
  return (
187
193
  <SelectPrimitive.Label
188
194
  ref={ref}
189
- className={cn("px-2 py-1.5 text-xs font-semibold text-muted-foreground", className)}
195
+ className={cn("px-2 py-1.5 text-meta font-semibold text-muted-foreground", className)}
190
196
  {...props}
191
197
  />
192
198
  );
@@ -200,7 +206,7 @@ export const SelectItem = forwardRef<
200
206
  <SelectPrimitive.Item
201
207
  ref={ref}
202
208
  className={cn(
203
- "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 ps-2 pe-8 text-sm outline-none transition-colors duration-fast",
209
+ "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 ps-2 pe-8 text-body outline-none transition-colors duration-fast",
204
210
  "focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
205
211
  className,
206
212
  )}
@@ -37,6 +37,7 @@ export const Separator = forwardRef<ElementRef<typeof SeparatorPrimitive.Root>,
37
37
  return (
38
38
  <SeparatorPrimitive.Root
39
39
  ref={ref}
40
+ data-slot="separator"
40
41
  decorative={decorative}
41
42
  orientation={orientation}
42
43
  className={cn(
@@ -0,0 +1,39 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "./sheet";
4
+
5
+ describe("SheetContent overflow", () => {
6
+ it("scrolls tall content instead of overflowing the fixed panel", () => {
7
+ render(
8
+ <Sheet open>
9
+ <SheetContent>
10
+ <SheetHeader>
11
+ <SheetTitle>Title</SheetTitle>
12
+ <SheetDescription>Description</SheetDescription>
13
+ </SheetHeader>
14
+ Body
15
+ </SheetContent>
16
+ </Sheet>,
17
+ );
18
+ const content = screen.getByRole("dialog");
19
+ // Without `overflow-y-auto`, content taller than the panel overflows the
20
+ // fixed-position box and its bottom half is unreachable.
21
+ expect(content.className).toContain("overflow-y-auto");
22
+ });
23
+
24
+ it("caps top/bottom sheets at a viewport-relative height so the scroll rule engages", () => {
25
+ render(
26
+ <Sheet open>
27
+ <SheetContent side="bottom">
28
+ <SheetHeader>
29
+ <SheetTitle>Title</SheetTitle>
30
+ <SheetDescription>Description</SheetDescription>
31
+ </SheetHeader>
32
+ Body
33
+ </SheetContent>
34
+ </Sheet>,
35
+ );
36
+ const content = screen.getByRole("dialog");
37
+ expect(content.className).toContain("max-h-[90dvh]");
38
+ });
39
+ });
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import {
2
4
  forwardRef,
3
5
  type ComponentPropsWithoutRef,
@@ -8,6 +10,7 @@ import * as SheetPrimitive from "@radix-ui/react-dialog";
8
10
  import { cva, type VariantProps } from "class-variance-authority";
9
11
  import { X } from "lucide-react";
10
12
  import { cn } from "../../lib/cn";
13
+ import { useLocale } from "../locale-provider";
11
14
 
12
15
  export const Sheet = SheetPrimitive.Root;
13
16
  export const SheetTrigger = SheetPrimitive.Trigger;
@@ -21,8 +24,9 @@ const SheetOverlay = forwardRef<
21
24
  return (
22
25
  <SheetPrimitive.Overlay
23
26
  ref={ref}
27
+ data-slot="sheet-overlay"
24
28
  className={cn(
25
- "fixed inset-0 z-50 bg-foreground/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
29
+ "fixed inset-0 z-50 bg-overlay backdrop-blur-overlay data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
26
30
  className,
27
31
  )}
28
32
  {...props}
@@ -31,13 +35,23 @@ const SheetOverlay = forwardRef<
31
35
  });
32
36
 
33
37
  const sheetVariants = cva(
34
- "fixed z-50 gap-4 overscroll-contain bg-card p-6 text-card-foreground shadow-ring-lg transition duration-slow ease-standard data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:[--tw-ease:var(--ease-entrance)] data-[state=closed]:[--tw-ease:var(--ease-exit)]",
38
+ // `overflow-y-auto` so content taller than the sheet scrolls INSIDE it
39
+ // instead of overflowing the fixed-position box (unreachable below the
40
+ // viewport edge).
41
+ "fixed z-50 gap-4 overflow-y-auto overscroll-contain bg-card p-6 text-card-foreground shadow-ring-lg transition duration-slow ease-standard data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:[--tw-ease:var(--ease-entrance)] data-[state=closed]:[--tw-ease:var(--ease-exit)]",
35
42
  {
36
43
  variants: {
37
44
  side: {
38
- top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
45
+ // `top`/`bottom` have no inherent height ceiling, so long content grows
46
+ // past the viewport; cap it at 90dvh (matching Dialog's dvh unit — ADR
47
+ // on mobile browser chrome) so the scroll rule above actually engages.
48
+ top: "inset-x-0 top-0 max-h-[90dvh] border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39
49
  bottom:
40
- "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
50
+ "inset-x-0 bottom-0 max-h-[90dvh] border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
51
+ // `side` is an explicit physical edge of the screen (like Radix's own
52
+ // Popper `side`), not a logical start/end — a caller who picks `"left"`
53
+ // wants the panel on the physical left even in an RTL layout, so these
54
+ // stay physical `left-0`/`right-0` and `border-r`/`border-l` on purpose.
41
55
  left: "inset-y-0 left-0 h-full w-3/4 max-w-sm border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left",
42
56
  right:
43
57
  "inset-y-0 right-0 h-full w-3/4 max-w-sm border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right",
@@ -56,18 +70,24 @@ export const SheetContent = forwardRef<
56
70
  ElementRef<typeof SheetPrimitive.Content>,
57
71
  SheetContentProps
58
72
  >(function SheetContent({ side = "right", className, children, ...props }, ref) {
73
+ const { t } = useLocale();
59
74
  return (
60
75
  <SheetPortal>
61
76
  <SheetOverlay />
62
77
  <SheetPrimitive.Content
63
78
  ref={ref}
79
+ data-slot="sheet-content"
64
80
  className={cn(sheetVariants({ side }), className)}
65
81
  {...props}
66
82
  >
67
83
  {children}
84
+ {/* Matches Dialog's close button exactly (radius, padding, ink,
85
+ opacity choreography) — a 24px target (`p-1` + the 16px glyph)
86
+ instead of the previous bare 16px hit area (#286). */}
68
87
  <SheetPrimitive.Close
69
- className="absolute end-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
70
- aria-label="Close"
88
+ data-slot="sheet-close"
89
+ className="absolute end-4 top-4 rounded-md p-1 text-muted-foreground opacity-70 transition-opacity hover:opacity-100 focus-ring"
90
+ aria-label={t("close")}
71
91
  >
72
92
  <X className="size-4" />
73
93
  </SheetPrimitive.Close>
@@ -77,11 +97,14 @@ export const SheetContent = forwardRef<
77
97
  });
78
98
 
79
99
  export function SheetHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
80
- return <div className={cn("flex flex-col gap-1.5", className)} {...props} />;
100
+ return (
101
+ <div data-slot="sheet-header" className={cn("flex flex-col gap-1.5", className)} {...props} />
102
+ );
81
103
  }
82
104
  export function SheetFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
83
105
  return (
84
106
  <div
107
+ data-slot="sheet-footer"
85
108
  className={cn("mt-auto flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
86
109
  {...props}
87
110
  />
@@ -92,7 +115,14 @@ export const SheetTitle = forwardRef<
92
115
  ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
93
116
  >(function SheetTitle({ className, ...props }, ref) {
94
117
  return (
95
- <SheetPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
118
+ // `text-title`, matching `DialogTitle` (#286) — was `text-lg font-semibold`,
119
+ // one rung below Dialog's for the same anatomical role (a modal title).
120
+ <SheetPrimitive.Title
121
+ ref={ref}
122
+ data-slot="sheet-title"
123
+ className={cn("text-title leading-none", className)}
124
+ {...props}
125
+ />
96
126
  );
97
127
  });
98
128
  export const SheetDescription = forwardRef<
@@ -102,7 +132,8 @@ export const SheetDescription = forwardRef<
102
132
  return (
103
133
  <SheetPrimitive.Description
104
134
  ref={ref}
105
- className={cn("text-sm text-muted-foreground", className)}
135
+ data-slot="sheet-description"
136
+ className={cn("text-body text-muted-foreground", className)}
106
137
  {...props}
107
138
  />
108
139
  );
@@ -0,0 +1 @@
1
+ export { SideDock, type SideDockProps } from "./side-dock";
@@ -0,0 +1,137 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, waitFor } from "storybook/test";
3
+
4
+ import { SideDock } from "./side-dock";
5
+
6
+ const meta = {
7
+ title: "Layout/SideDock",
8
+ component: SideDock,
9
+ tags: ["autodocs"],
10
+ parameters: {
11
+ layout: "fullscreen",
12
+ docs: {
13
+ description: {
14
+ component:
15
+ "A summoned, resizable panel that docks to one edge of the content area — closed by default, opened by something the user does, resizable by pointer and keyboard, and rendered as an overlay `Sheet` below `overlayBreakpoint` (default 1100) instead of a fixed-width column. Unlike `ContextRail` (a persistent rail with a 48px icon collapsed state), `SideDock` has no persistent strip: closed means gone. See `docs/ADR/0035-context-rail-and-side-dock.md` §5.",
16
+ },
17
+ },
18
+ },
19
+ } satisfies Meta<typeof SideDock>;
20
+ export default meta;
21
+ type Story = StoryObj<typeof meta>;
22
+
23
+ const dockBody = <p className="text-body text-muted-foreground">Dock body content.</p>;
24
+
25
+ /**
26
+ * Open, right-hand (the default `side`). The play function tabs from the
27
+ * close button to the resize handle, widens it twice with the keyboard
28
+ * (`ArrowLeft` on a right-hand dock), and asserts the CONTAINER's actual
29
+ * rendered width grew — a real layout measurement, not just the `aria-*`
30
+ * bookkeeping the unit tests already cover.
31
+ */
32
+ export const Default: Story = {
33
+ render: () => (
34
+ <div className="flex h-[560px] w-full">
35
+ <div className="flex-1 bg-background p-6 text-muted-foreground">Canvas content</div>
36
+ <SideDock title="Assistant" open>
37
+ {dockBody}
38
+ </SideDock>
39
+ </div>
40
+ ),
41
+ play: async ({ canvas, canvasElement, userEvent }) => {
42
+ const closeButton = canvas.getByRole("button", { name: "Close" });
43
+ const handle = canvas.getByRole("separator");
44
+ const container = canvasElement.querySelector(
45
+ '[data-slot="side-dock-container"]',
46
+ ) as HTMLElement;
47
+
48
+ await userEvent.tab();
49
+ await expect(closeButton).toHaveFocus();
50
+
51
+ /* The scroll port sits BETWEEN the two, and that is the point: a dock's
52
+ * children are caller-supplied and routinely have no focusable descendant,
53
+ * so before this stop existed a keyboard user could reach the close button
54
+ * and the resize handle but never the region that actually scrolls (WCAG
55
+ * 2.1.1, axe `scrollable-region-focusable`). `getAttribute`, not the
56
+ * `tabIndex` IDL getter, which reads -1 whether or not the attribute is
57
+ * there; and `classList`, because `toHaveClass` is a subset check that
58
+ * would pass with BOTH rungs applied — the inset rung is required here
59
+ * because the dock container clips anything drawn outside this box. */
60
+ const body = canvasElement.querySelector('[data-slot="side-dock-body"]') as HTMLElement;
61
+ await userEvent.tab();
62
+ await expect(body).toHaveFocus();
63
+ await expect(body.getAttribute("tabindex")).toBe("0");
64
+ await expect(body.classList.contains("focus-ring-inset")).toBe(true);
65
+ await expect(body.classList.contains("focus-ring")).toBe(false);
66
+
67
+ await userEvent.tab();
68
+ await expect(handle).toHaveFocus();
69
+
70
+ const before = container.getBoundingClientRect().width;
71
+ await userEvent.keyboard("{ArrowLeft}");
72
+ await userEvent.keyboard("{ArrowLeft}");
73
+
74
+ await waitFor(() => {
75
+ const after = container.getBoundingClientRect().width;
76
+ expect(after).toBeGreaterThan(before);
77
+ });
78
+ },
79
+ };
80
+
81
+ /** `open={false}` (the default) — collapsed, `inert`, off-screen; its body content stays mounted for the closing transition. */
82
+ export const Closed: Story = {
83
+ render: () => (
84
+ <div className="flex h-[560px] w-full">
85
+ <div className="flex-1 bg-background p-6 text-muted-foreground">Canvas content</div>
86
+ <SideDock title="Assistant">{dockBody}</SideDock>
87
+ </div>
88
+ ),
89
+ play: async ({ canvasElement }) => {
90
+ const root = canvasElement.querySelector('[data-slot="side-dock"]');
91
+ const container = canvasElement.querySelector('[data-slot="side-dock-container"]');
92
+ await expect(root).toHaveAttribute("data-state", "collapsed");
93
+ await expect(container).toHaveAttribute("inert");
94
+ },
95
+ };
96
+
97
+ /** `side="left"` — the handle sits on the trailing edge and the keyboard direction inverts (`ArrowRight` widens). */
98
+ export const LeftHanded: Story = {
99
+ render: () => (
100
+ <div className="flex h-[560px] w-full">
101
+ <SideDock title="Assistant" open side="left">
102
+ {dockBody}
103
+ </SideDock>
104
+ <div className="flex-1 bg-background p-6 text-muted-foreground">Canvas content</div>
105
+ </div>
106
+ ),
107
+ play: async ({ canvas }) => {
108
+ const handle = canvas.getByRole("separator");
109
+ await expect(handle).toHaveAttribute("aria-orientation", "vertical");
110
+ },
111
+ };
112
+
113
+ /**
114
+ * Below `overlayBreakpoint` the dock renders as an overlay `Sheet` instead of
115
+ * a column, with no resize handle. A very high `overlayBreakpoint` forces
116
+ * this branch deterministically under a headless run, mirroring
117
+ * `ContextRail`'s own `Narrow` story.
118
+ */
119
+ export const Overlay: Story = {
120
+ render: () => (
121
+ <div className="flex h-[560px] w-full">
122
+ <div className="flex-1 bg-background p-6 text-muted-foreground">Canvas content</div>
123
+ <SideDock title="Assistant" open overlayBreakpoint={4000} description="A helper panel.">
124
+ {dockBody}
125
+ </SideDock>
126
+ </div>
127
+ ),
128
+ play: async ({ canvasElement }) => {
129
+ const dialog = await waitFor(() => {
130
+ const el = document.querySelector('[role="dialog"]');
131
+ if (!el) throw new Error("overlay Sheet has not mounted yet");
132
+ return el;
133
+ });
134
+ await expect(dialog).toHaveAccessibleName("Assistant");
135
+ await expect(canvasElement.querySelector('[data-slot="side-dock-resize-handle"]')).toBeNull();
136
+ },
137
+ };