@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,11 +1,21 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react-vite";
2
2
  import { expect, userEvent, within } from "storybook/test";
3
+ import { Toaster } from "../sonner";
3
4
  import { CopyableValue } from "./copyable-value";
4
5
 
5
6
  const meta = {
6
7
  title: "Display/CopyableValue",
7
8
  component: CopyableValue,
8
9
  tags: ["autodocs"],
10
+ // The copy toast needs a host; apps mount one `<Toaster />` at their root.
11
+ decorators: [
12
+ (Story) => (
13
+ <>
14
+ <Story />
15
+ <Toaster />
16
+ </>
17
+ ),
18
+ ],
9
19
  parameters: {
10
20
  docs: {
11
21
  description: {
@@ -11,7 +11,10 @@
11
11
  import { describe, expect, it, vi } from "vitest";
12
12
  import { render, screen, fireEvent, waitFor } from "@testing-library/react";
13
13
  import userEvent from "@testing-library/user-event";
14
- import { CopyableValue } from "./copyable-value";
14
+ import { toast } from "../sonner";
15
+ import { CopyableValue, COPYABLE_VALUE_TOAST_MS } from "./copyable-value";
16
+
17
+ vi.mock("../sonner", () => ({ toast: vi.fn() }));
15
18
 
16
19
  /** Install a clipboard stub for one test; returns the writeText spy. */
17
20
  function stubClipboard(impl: () => Promise<void> = () => Promise.resolve()) {
@@ -38,6 +41,30 @@ describe("CopyableValue", () => {
38
41
  });
39
42
  });
40
43
 
44
+ it("shows a one-second toast once the copy succeeds", async () => {
45
+ vi.mocked(toast).mockClear();
46
+ stubClipboard();
47
+ render(<CopyableValue value="42">42</CopyableValue>);
48
+
49
+ fireEvent.click(screen.getByRole("button"));
50
+ await waitFor(() => {
51
+ expect(toast).toHaveBeenCalledWith("Copied", { duration: COPYABLE_VALUE_TOAST_MS });
52
+ });
53
+ expect(COPYABLE_VALUE_TOAST_MS).toBe(1000);
54
+ });
55
+
56
+ it("shows no toast when the copy fails", async () => {
57
+ vi.mocked(toast).mockClear();
58
+ stubClipboard(() => Promise.reject(new Error("denied")));
59
+ render(<CopyableValue value="42">42</CopyableValue>);
60
+
61
+ fireEvent.click(screen.getByRole("button"));
62
+ await waitFor(() => {
63
+ expect(screen.getByRole("status")).toHaveTextContent("");
64
+ });
65
+ expect(toast).not.toHaveBeenCalled();
66
+ });
67
+
41
68
  it("falls back to the exact value as its own display", () => {
42
69
  render(<CopyableValue value="1234" />);
43
70
  expect(screen.getByRole("button")).toHaveTextContent("1234");
@@ -19,6 +19,10 @@ import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
19
19
  import { cn } from "../../lib/cn";
20
20
  import { useCopyToClipboard } from "../../lib/use-copy-to-clipboard";
21
21
  import { useLocale } from "../locale-provider";
22
+ import { toast } from "../sonner";
23
+
24
+ /** ms the copy-confirmation toast stays on screen. */
25
+ export const COPYABLE_VALUE_TOAST_MS = 1000;
22
26
 
23
27
  export interface CopyableValueProps extends Omit<
24
28
  ButtonHTMLAttributes<HTMLButtonElement>,
@@ -72,14 +76,21 @@ export const CopyableValue = forwardRef<HTMLButtonElement, CopyableValueProps>(
72
76
  // only resting affordance, so a value inside a heading or a KPI
73
77
  // still looks like a value, not a link.
74
78
  "inline-flex items-baseline gap-1 rounded-sm underline decoration-dotted decoration-muted-foreground underline-offset-4",
75
- "hover:decoration-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
79
+ "hover:decoration-foreground focus-ring",
76
80
  "transition-colors duration-fast ease-standard motion-reduce:transition-none",
77
81
  className,
78
82
  )}
79
83
  onClick={(event) => {
80
84
  onClick?.(event);
81
85
  if (!event.defaultPrevented) {
82
- void copy(value);
86
+ // The toast is the visible confirmation; it needs an app-mounted
87
+ // `<Toaster />` and is a silent no-op without one, which is why
88
+ // the live region below stays as the always-present fallback.
89
+ void copy(value).then((ok) => {
90
+ if (ok) {
91
+ toast(t("ui.copyableValue.copied"), { duration: COPYABLE_VALUE_TOAST_MS });
92
+ }
93
+ });
83
94
  }
84
95
  }}
85
96
  >
@@ -5,6 +5,15 @@ const meta = {
5
5
  title: "Forms/DatePicker",
6
6
  component: DatePicker,
7
7
  tags: ["autodocs"],
8
+ // The trigger fills its container by default (no hard-fixed width) — cap
9
+ // the docs canvas to a realistic form-field width.
10
+ decorators: [
11
+ (Story) => (
12
+ <div className="w-full max-w-xs">
13
+ <Story />
14
+ </div>
15
+ ),
16
+ ],
8
17
  argTypes: {
9
18
  value: {
10
19
  description: "Controlled selected date.",
@@ -0,0 +1,52 @@
1
+ import { createRef } from "react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { render, screen, within } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+ import { DatePicker } from "./date-picker";
6
+ import { LocaleProvider } from "../locale-provider";
7
+
8
+ describe("DatePicker", () => {
9
+ it("forwards the ref to the trigger button", () => {
10
+ const ref = createRef<HTMLButtonElement>();
11
+ render(<DatePicker ref={ref} />);
12
+ expect(ref.current).toBeInstanceOf(HTMLButtonElement);
13
+ expect(ref.current).toBe(screen.getByRole("button"));
14
+ });
15
+
16
+ it("does not hard-fix the trigger width — fills its container by default", () => {
17
+ render(<DatePicker />);
18
+ expect(screen.getByRole("button").className).not.toMatch(/\bw-56\b/);
19
+ });
20
+
21
+ // #reviewed 1.7 — same `value ?? internal` / ternary-setter bug as Combobox:
22
+ // an uncontrolled DatePicker with an onValueChange listener never updated
23
+ // its own displayed date.
24
+ it("updates its own displayed date when uncontrolled even with an onValueChange listener", async () => {
25
+ const onValueChange = vi.fn();
26
+ const user = userEvent.setup();
27
+ render(<DatePicker onValueChange={onValueChange} placeholder="Pick a date" />);
28
+ const trigger = screen.getByRole("button", { name: /pick a date/i });
29
+ await user.click(trigger);
30
+ const grid = await screen.findByRole("grid");
31
+ const dayButtons = within(grid)
32
+ .getAllByRole("button")
33
+ .filter((b) => !/previous|next/i.test(b.getAttribute("aria-label") ?? ""));
34
+ const firstEnabled = dayButtons.find((b) => !(b as HTMLButtonElement).disabled)!;
35
+ await user.click(firstEnabled);
36
+ expect(onValueChange).toHaveBeenCalled();
37
+ expect(trigger).not.toHaveTextContent("Pick a date");
38
+ });
39
+
40
+ it("formats the displayed date using the active locale", () => {
41
+ const date = new Date(2024, 0, 15); // Jan 15, 2024
42
+ render(
43
+ <LocaleProvider locale="de-DE">
44
+ <DatePicker value={date} />
45
+ </LocaleProvider>,
46
+ );
47
+ // de-DE "medium" dateStyle renders like "15. Jan. 2024" — distinct from
48
+ // the en-US default ("Jan 15, 2024").
49
+ expect(screen.getByRole("button")).toHaveTextContent("2024");
50
+ expect(screen.getByRole("button").textContent).not.toMatch(/Jan 15, 2024/);
51
+ });
52
+ });
@@ -1,8 +1,10 @@
1
- import { useState } from "react";
1
+ import { forwardRef } from "react";
2
2
  import { Calendar as CalendarIcon } from "lucide-react";
3
3
  import { cn } from "../../lib/cn";
4
+ import { useControllableState } from "../../lib/use-controllable-state";
4
5
  import { Button } from "../button";
5
6
  import { Calendar } from "../calendar";
7
+ import { useLocale } from "../locale-provider";
6
8
  import { Popover, PopoverContent, PopoverTrigger } from "../popover";
7
9
 
8
10
  export interface DatePickerProps {
@@ -13,26 +15,22 @@ export interface DatePickerProps {
13
15
  }
14
16
 
15
17
  /** Single-date picker (Popover + Calendar). Controlled or uncontrolled. */
16
- export function DatePicker({
17
- value,
18
- onValueChange,
19
- placeholder = "Pick a date",
20
- className,
21
- }: DatePickerProps) {
22
- const [internal, setInternal] = useState<Date | undefined>(undefined);
23
- const date = value ?? internal;
24
- const setDate = (d?: Date) => (onValueChange ? onValueChange(d) : setInternal(d));
25
- const label = date
26
- ? new Intl.DateTimeFormat(undefined, { dateStyle: "medium" }).format(date)
27
- : placeholder;
18
+ export const DatePicker = forwardRef<HTMLButtonElement, DatePickerProps>(function DatePicker(
19
+ { value, onValueChange, placeholder = "Pick a date", className },
20
+ ref,
21
+ ) {
22
+ const { formatDate } = useLocale();
23
+ const [date, setDate] = useControllableState<Date | undefined>(value, undefined, onValueChange);
24
+ const label = date ? formatDate(date, { dateStyle: "medium" }) : placeholder;
28
25
 
29
26
  return (
30
27
  <Popover>
31
28
  <PopoverTrigger asChild>
32
29
  <Button
30
+ ref={ref}
33
31
  variant="outline"
34
32
  className={cn(
35
- "w-56 justify-start gap-2 text-start font-normal",
33
+ "w-full justify-start gap-2 text-start font-normal",
36
34
  !date && "text-muted-foreground",
37
35
  className,
38
36
  )}
@@ -46,4 +44,4 @@ export function DatePicker({
46
44
  </PopoverContent>
47
45
  </Popover>
48
46
  );
49
- }
47
+ });
@@ -12,6 +12,15 @@ const meta = {
12
12
  parameters: {
13
13
  layout: "centered",
14
14
  },
15
+ // The trigger fills its container by default (no hard-fixed width) — cap
16
+ // the docs canvas to a realistic form-field width.
17
+ decorators: [
18
+ (Story) => (
19
+ <div className="w-80">
20
+ <Story />
21
+ </div>
22
+ ),
23
+ ],
15
24
  argTypes: {
16
25
  value: {
17
26
  description: "Controlled date range `{ from, to }`.",
@@ -6,9 +6,12 @@
6
6
  * select interaction and accessibility are covered by the Storybook stories
7
7
  * (run via `pnpm --filter @elabs-ai/components-docs test-storybook` or the MCP story tests).
8
8
  */
9
+ import { useState } from "react";
9
10
  import { describe, expect, it } from "vitest";
10
11
  import { render, screen } from "@testing-library/react";
11
- import { DateRangePicker } from "./date-range-picker";
12
+ import userEvent from "@testing-library/user-event";
13
+ import { DateRangePicker, type DateRange } from "./date-range-picker";
14
+ import { LocaleProvider } from "../locale-provider";
12
15
 
13
16
  describe("DateRangePicker", () => {
14
17
  it("renders the placeholder on the trigger when no range is set", () => {
@@ -59,4 +62,76 @@ describe("DateRangePicker", () => {
59
62
  const button = screen.getByRole("button");
60
63
  expect(button.className).toContain("my-custom-class");
61
64
  });
65
+
66
+ it("does not hard-fix the trigger width — fills its container by default", () => {
67
+ render(<DateRangePicker />);
68
+ expect(screen.getByRole("button").className).not.toMatch(/\bw-72\b/);
69
+ });
70
+
71
+ it("spreads extra props (e.g. data-testid) onto the trigger button", () => {
72
+ render(<DateRangePicker data-testid="range-trigger" />);
73
+ expect(screen.getByTestId("range-trigger")).toBeInTheDocument();
74
+ });
75
+
76
+ it("formats the range label using the active locale", () => {
77
+ const from = new Date(2024, 0, 5);
78
+ const to = new Date(2024, 0, 20);
79
+ render(
80
+ <LocaleProvider locale="de-DE">
81
+ <DateRangePicker value={{ from, to }} />
82
+ </LocaleProvider>,
83
+ );
84
+ // de-DE renders "5. Jan. 2024" style — distinct from the en-US default.
85
+ expect(screen.getByRole("button").textContent).not.toMatch(/Jan 5, 2024/);
86
+ });
87
+
88
+ // #reviewed 1.7 — clearing a controlled range to `undefined` must not fall
89
+ // through to a stale uncontrolled `internal` value: once controlled, the
90
+ // component must stay controlled for its whole lifetime.
91
+ it("collapses to a single month below the sm breakpoint (phone-width popover)", async () => {
92
+ const originalWidth = window.innerWidth;
93
+ Object.defineProperty(window, "innerWidth", {
94
+ writable: true,
95
+ configurable: true,
96
+ value: 375,
97
+ });
98
+ try {
99
+ const user = userEvent.setup();
100
+ render(<DateRangePicker />);
101
+ await user.click(screen.getByRole("button"));
102
+ const grids = await screen.findAllByRole("grid");
103
+ expect(grids).toHaveLength(1);
104
+ } finally {
105
+ Object.defineProperty(window, "innerWidth", {
106
+ writable: true,
107
+ configurable: true,
108
+ value: originalWidth,
109
+ });
110
+ }
111
+ });
112
+
113
+ it("shows the placeholder (not a stale value) when a controlled range is cleared to undefined", async () => {
114
+ function Harness() {
115
+ const [range, setRange] = useState<DateRange | undefined>({
116
+ from: new Date(2024, 0, 5),
117
+ to: new Date(2024, 0, 20),
118
+ });
119
+ return (
120
+ <>
121
+ <DateRangePicker value={range} onValueChange={setRange} placeholder="Pick a range" />
122
+ <button type="button" onClick={() => setRange(undefined)}>
123
+ Clear
124
+ </button>
125
+ </>
126
+ );
127
+ }
128
+ const user = userEvent.setup();
129
+ render(<Harness />);
130
+ const trigger = screen.getByRole("button", { name: /–/ });
131
+ expect(trigger.textContent).not.toBe("Pick a range");
132
+
133
+ await user.click(screen.getByRole("button", { name: "Clear" }));
134
+
135
+ expect(trigger).toHaveTextContent("Pick a range");
136
+ });
62
137
  });
@@ -1,14 +1,20 @@
1
1
  // a2ui.exposed: yes
2
2
  "use client";
3
3
 
4
- import { forwardRef, useState } from "react";
4
+ import { forwardRef, type HTMLAttributes } from "react";
5
5
  import { Calendar as CalendarIcon } from "lucide-react";
6
6
  import type { DateRange } from "react-day-picker";
7
7
  import { cn } from "../../lib/cn";
8
+ import { useControllableState } from "../../lib/use-controllable-state";
9
+ import { useIsMobile } from "../../lib/use-mobile";
8
10
  import { Button } from "../button";
9
11
  import { Calendar } from "../calendar";
12
+ import { useLocale } from "../locale-provider";
10
13
  import { Popover, PopoverContent, PopoverTrigger } from "../popover";
11
14
 
15
+ /** Below this width, the calendar collapses to a single month (Tailwind `sm`). */
16
+ const NARROW_BREAKPOINT = 640;
17
+
12
18
  export type { DateRange };
13
19
 
14
20
  export interface DateRangePreset {
@@ -16,7 +22,10 @@ export interface DateRangePreset {
16
22
  getRange: () => DateRange;
17
23
  }
18
24
 
19
- export interface DateRangePickerProps {
25
+ export interface DateRangePickerProps extends Omit<
26
+ HTMLAttributes<HTMLButtonElement>,
27
+ "onSelect" | "children" | "value" | "defaultValue"
28
+ > {
20
29
  value?: DateRange;
21
30
  defaultValue?: DateRange;
22
31
  onValueChange?: (range?: DateRange) => void;
@@ -26,16 +35,6 @@ export interface DateRangePickerProps {
26
35
  className?: string;
27
36
  }
28
37
 
29
- const fmt = (date: Date) =>
30
- new Intl.DateTimeFormat(undefined, { dateStyle: "medium" }).format(date);
31
-
32
- function formatRange(range: DateRange | undefined, placeholder: string): string {
33
- if (!range) return placeholder;
34
- if (range.from && range.to) return `${fmt(range.from)} – ${fmt(range.to)}`;
35
- if (range.from) return `${fmt(range.from)} – …`;
36
- return placeholder;
37
- }
38
-
39
38
  /**
40
39
  * Date range picker (Popover + Calendar in range mode).
41
40
  * Supports optional preset quick-picks, controlled or uncontrolled.
@@ -50,24 +49,31 @@ export const DateRangePicker = forwardRef<HTMLButtonElement, DateRangePickerProp
50
49
  numberOfMonths = 2,
51
50
  placeholder = "Pick a date range",
52
51
  className,
52
+ ...props
53
53
  },
54
54
  ref,
55
55
  ) {
56
- const [internal, setInternal] = useState<DateRange | undefined>(defaultValue);
57
-
58
- const isControlled = value !== undefined;
59
- const range = isControlled ? value : internal;
56
+ const { formatDate } = useLocale();
57
+ const [range, setRange] = useControllableState<DateRange | undefined>(
58
+ value,
59
+ defaultValue,
60
+ onValueChange,
61
+ );
60
62
 
61
- const setRange = (r?: DateRange) => {
62
- if (!isControlled) {
63
- setInternal(r);
64
- }
65
- // Always fire the callback so uncontrolled consumers can observe changes
66
- onValueChange?.(r);
63
+ const fmt = (date: Date) => formatDate(date, { dateStyle: "medium" });
64
+ const formatRange = (r: DateRange | undefined): string => {
65
+ if (!r) return placeholder;
66
+ if (r.from && r.to) return `${fmt(r.from)} – ${fmt(r.to)}`;
67
+ if (r.from) return `${fmt(r.from)} …`;
68
+ return placeholder;
67
69
  };
68
70
 
69
71
  const hasRange = Boolean(range?.from);
70
- const label = formatRange(range, placeholder);
72
+ const label = formatRange(range);
73
+
74
+ // Below `sm`, two side-by-side months push the popover off-screen (#reviewed).
75
+ const isNarrow = useIsMobile(NARROW_BREAKPOINT);
76
+ const monthsToShow = isNarrow ? 1 : numberOfMonths;
71
77
 
72
78
  return (
73
79
  <Popover>
@@ -76,8 +82,9 @@ export const DateRangePicker = forwardRef<HTMLButtonElement, DateRangePickerProp
76
82
  ref={ref}
77
83
  variant="outline"
78
84
  aria-haspopup="dialog"
85
+ {...props}
79
86
  className={cn(
80
- "w-72 justify-start gap-2 text-start font-normal",
87
+ "w-full justify-start gap-2 text-start font-normal",
81
88
  !hasRange && "text-muted-foreground",
82
89
  className,
83
90
  )}
@@ -86,10 +93,14 @@ export const DateRangePicker = forwardRef<HTMLButtonElement, DateRangePickerProp
86
93
  {label}
87
94
  </Button>
88
95
  </PopoverTrigger>
89
- <PopoverContent className="w-auto p-0" align="start">
96
+ <PopoverContent
97
+ className="w-auto max-w-[calc(100vw-1rem)] p-0"
98
+ align="start"
99
+ collisionPadding={8}
100
+ >
90
101
  {presets && presets.length > 0 ? (
91
102
  <div className="flex">
92
- <div className="flex flex-col gap-1 border-r border-border p-2">
103
+ <div className="flex flex-col gap-1 border-e border-border p-2">
93
104
  {presets.map((preset) => (
94
105
  <Button
95
106
  key={preset.label}
@@ -104,7 +115,7 @@ export const DateRangePicker = forwardRef<HTMLButtonElement, DateRangePickerProp
104
115
  </div>
105
116
  <Calendar
106
117
  mode="range"
107
- numberOfMonths={numberOfMonths}
118
+ numberOfMonths={monthsToShow}
108
119
  selected={range}
109
120
  onSelect={setRange}
110
121
  autoFocus
@@ -113,7 +124,7 @@ export const DateRangePicker = forwardRef<HTMLButtonElement, DateRangePickerProp
113
124
  ) : (
114
125
  <Calendar
115
126
  mode="range"
116
- numberOfMonths={numberOfMonths}
127
+ numberOfMonths={monthsToShow}
117
128
  selected={range}
118
129
  onSelect={setRange}
119
130
  autoFocus
@@ -3,7 +3,7 @@ import { createContext, forwardRef, use, type HTMLAttributes } from "react";
3
3
  import { cva, type VariantProps } from "class-variance-authority";
4
4
  import { cn } from "../../lib/cn";
5
5
 
6
- const descriptionsVariants = cva("grid gap-x-6 gap-y-3 text-sm", {
6
+ const descriptionsVariants = cva("grid gap-x-6 gap-y-3 text-body", {
7
7
  variants: {
8
8
  columns: {
9
9
  1: "grid-cols-1",
@@ -47,11 +47,16 @@ describe("DialogContent size variants", () => {
47
47
  expect(content.className).toContain("max-w-3xl");
48
48
  });
49
49
 
50
- it("size='full' renders max-w-[95vw] and h-[90vh]", () => {
50
+ it("size='full' renders max-w-[95vw] and h-[90dvh] — dvh, not vh (mobile browser chrome)", () => {
51
51
  render(<Wrapper size="full" />);
52
52
  const content = screen.getByRole("dialog");
53
53
  expect(content.className).toContain("max-w-[95vw]");
54
- expect(content.className).toContain("h-[90vh]");
54
+ expect(content.className).toContain("h-[90dvh]");
55
+ expect(content.className).toContain("max-h-[90dvh]");
56
+ // `vh` on mobile Safari/Chrome is measured against the viewport with the
57
+ // browser chrome collapsed, so a plain `vh` value overflows past what is
58
+ // actually visible while the address bar is shown.
59
+ expect(content.className).not.toMatch(/(?<!d)vh\]/);
55
60
  });
56
61
  });
57
62
 
@@ -106,7 +111,7 @@ describe("DialogContent height/scroll regime (#341)", () => {
106
111
  // later variant utility, so `full` still lets its body own the scroll.
107
112
  expect(content.className).toContain("overflow-hidden");
108
113
  expect(content.className).not.toContain("overflow-y-auto");
109
- expect(content.className).toContain("max-h-[90vh]");
114
+ expect(content.className).toContain("max-h-[90dvh]");
110
115
  expect(content.className).not.toContain("max-h-[calc(100dvh-2rem)]");
111
116
  });
112
117
  });
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import {
2
4
  forwardRef,
3
5
  useCallback,
@@ -11,6 +13,7 @@ import {
11
13
  import * as DialogPrimitive from "@radix-ui/react-dialog";
12
14
  import { cva, type VariantProps } from "class-variance-authority";
13
15
  import { cn } from "../../lib/cn";
16
+ import { useLocale } from "../locale-provider";
14
17
  import { Heading, Text } from "../typography";
15
18
 
16
19
  /**
@@ -55,7 +58,11 @@ export const dialogContentVariants = cva(
55
58
  sm: "max-w-sm",
56
59
  lg: "max-w-lg",
57
60
  xl: "max-w-3xl",
58
- full: "max-w-[95vw] h-[90vh] max-h-[90vh] overflow-hidden",
61
+ // `dvh` (dynamic viewport height), not `vh`: on mobile Safari/Chrome
62
+ // `vh` is measured against the LARGEST viewport (browser chrome
63
+ // collapsed), so `size="full"` overflows past the visible viewport
64
+ // while the address bar is showing.
65
+ full: "max-w-[95vw] h-[90dvh] max-h-[90dvh] overflow-hidden",
59
66
  },
60
67
  },
61
68
  defaultVariants: { size: "lg" },
@@ -76,7 +83,7 @@ export const DialogOverlay = forwardRef<
76
83
  ref={ref}
77
84
  data-slot="dialog-overlay"
78
85
  className={cn(
79
- "fixed inset-0 z-50 bg-foreground/50 backdrop-blur-sm",
86
+ "fixed inset-0 z-50 bg-overlay backdrop-blur-overlay",
80
87
  "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
81
88
  className,
82
89
  )}
@@ -90,6 +97,7 @@ export const DialogContent = forwardRef<
90
97
  ComponentPropsWithoutRef<typeof DialogPrimitive.Content> &
91
98
  VariantProps<typeof dialogContentVariants>
92
99
  >(function DialogContent({ className, children, size, onOpenAutoFocus, ...props }, ref) {
100
+ const { t } = useLocale();
93
101
  const contentRef = useRef<HTMLDivElement | null>(null);
94
102
  const setRefs = useCallback(
95
103
  (node: HTMLDivElement | null) => {
@@ -149,8 +157,8 @@ export const DialogContent = forwardRef<
149
157
  {children}
150
158
  <DialogPrimitive.Close
151
159
  data-slot="dialog-close"
152
- className="absolute end-4 top-4 rounded-md p-1 text-muted-foreground opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
153
- aria-label="Close"
160
+ className="absolute end-4 top-4 rounded-md p-1 text-muted-foreground opacity-70 transition-opacity hover:opacity-100 focus-ring"
161
+ aria-label={t("close")}
154
162
  >
155
163
  <svg
156
164
  width="16"
@@ -213,7 +221,7 @@ export const DialogDescription = forwardRef<
213
221
  <DialogPrimitive.Description
214
222
  ref={ref}
215
223
  data-slot="dialog-description"
216
- className={cn("text-sm text-muted-foreground", className)}
224
+ className={cn("text-body text-muted-foreground", className)}
217
225
  {...props}
218
226
  />
219
227
  );
@@ -275,7 +283,7 @@ export const DialogBody = forwardRef<HTMLDivElement, DialogBodyProps>(function D
275
283
  // focusable content.
276
284
  tabIndex={tabIndex ?? 0}
277
285
  className={cn(
278
- "-m-1 min-h-0 overflow-y-auto overscroll-contain p-1 scroll-p-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
286
+ "-m-1 min-h-0 overflow-y-auto overscroll-contain p-1 scroll-p-1 focus-ring",
279
287
  className,
280
288
  )}
281
289
  {...props}
@@ -25,7 +25,8 @@ const DrawerOverlay = forwardRef<
25
25
  return (
26
26
  <DrawerPrimitive.Overlay
27
27
  ref={ref}
28
- className={cn("fixed inset-0 z-50 bg-foreground/50", className)}
28
+ data-slot="drawer-overlay"
29
+ className={cn("fixed inset-0 z-50 bg-overlay", className)}
29
30
  {...props}
30
31
  />
31
32
  );
@@ -40,13 +41,14 @@ export const DrawerContent = forwardRef<
40
41
  <DrawerOverlay />
41
42
  <DrawerPrimitive.Content
42
43
  ref={ref}
44
+ data-slot="drawer-content"
43
45
  className={cn(
44
46
  "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col overscroll-contain rounded-t-xl border bg-card text-card-foreground",
45
47
  className,
46
48
  )}
47
49
  {...props}
48
50
  >
49
- <div className="mx-auto mt-4 h-1.5 w-12 rounded-full bg-muted" />
51
+ <div data-slot="drawer-handle" className="mx-auto mt-4 h-1.5 w-12 rounded-full bg-muted" />
50
52
  {children}
51
53
  </DrawerPrimitive.Content>
52
54
  </DrawerPortal>
@@ -54,19 +56,33 @@ export const DrawerContent = forwardRef<
54
56
  });
55
57
 
56
58
  export function DrawerHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
57
- return <div className={cn("grid gap-1.5 p-4 text-center sm:text-start", className)} {...props} />;
59
+ return (
60
+ <div
61
+ data-slot="drawer-header"
62
+ className={cn("grid gap-1.5 p-4 text-center sm:text-start", className)}
63
+ {...props}
64
+ />
65
+ );
58
66
  }
59
67
  export function DrawerFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
60
- return <div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />;
68
+ return (
69
+ <div
70
+ data-slot="drawer-footer"
71
+ className={cn("mt-auto flex flex-col gap-2 p-4", className)}
72
+ {...props}
73
+ />
74
+ );
61
75
  }
62
76
  export const DrawerTitle = forwardRef<
63
77
  ElementRef<typeof DrawerPrimitive.Title>,
64
78
  ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
65
79
  >(function DrawerTitle({ className, ...props }, ref) {
66
80
  return (
81
+ // `text-title`, matching `DialogTitle` (#286) — was `text-lg font-semibold`.
67
82
  <DrawerPrimitive.Title
68
83
  ref={ref}
69
- className={cn("text-lg font-semibold", className)}
84
+ data-slot="drawer-title"
85
+ className={cn("text-title leading-none", className)}
70
86
  {...props}
71
87
  />
72
88
  );
@@ -78,7 +94,8 @@ export const DrawerDescription = forwardRef<
78
94
  return (
79
95
  <DrawerPrimitive.Description
80
96
  ref={ref}
81
- className={cn("text-sm text-muted-foreground", className)}
97
+ data-slot="drawer-description"
98
+ className={cn("text-body text-muted-foreground", className)}
82
99
  {...props}
83
100
  />
84
101
  );
@@ -147,7 +147,7 @@ export const SubmenuRadioAndCheckbox: Story = {
147
147
  };
148
148
 
149
149
  /**
150
- * Three-theme sweep (a durable, `test-storybook`-run artifact rather than a
150
+ * Cross-theme sweep (a durable, `test-storybook`-run artifact rather than a
151
151
  * one-off manual `preview.tsx` toggle). `<ThemeProvider>` (not a plain
152
152
  * `data-theme` wrapper `<div>`) because it writes the attribute onto the
153
153
  * DOCUMENT ROOT — this menu's Radix `Portal` mounts its content on