@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
@@ -109,7 +109,7 @@ export const DropdownMenuContent = forwardRef<
109
109
  ref={ref}
110
110
  sideOffset={sideOffset}
111
111
  className={cn(
112
- "z-50 min-w-[10rem] overflow-hidden rounded-md bg-popover p-1 text-popover-foreground shadow-ring-md",
112
+ "z-50 min-w-[12rem] overflow-hidden rounded-md bg-popover p-1 text-popover-foreground shadow-ring-md",
113
113
  "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",
114
114
  "data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
115
115
  className,
@@ -128,7 +128,7 @@ export const DropdownMenuItem = forwardRef<
128
128
  <DropdownMenuPrimitive.Item
129
129
  ref={ref}
130
130
  className={cn(
131
- "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors duration-fast",
131
+ "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-body outline-none transition-colors duration-fast",
132
132
  "focus:bg-accent focus:text-accent-foreground",
133
133
  "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
134
134
  "[&_svg]:size-4 [&_svg]:shrink-0",
@@ -147,7 +147,7 @@ export const DropdownMenuLabel = forwardRef<
147
147
  return (
148
148
  <DropdownMenuPrimitive.Label
149
149
  ref={ref}
150
- className={cn("px-2 py-1.5 text-xs font-semibold text-muted-foreground", className)}
150
+ className={cn("px-2 py-1.5 text-meta font-semibold text-muted-foreground", className)}
151
151
  {...props}
152
152
  />
153
153
  );
@@ -172,7 +172,7 @@ export function DropdownMenuShortcut({
172
172
  }: React.HTMLAttributes<HTMLSpanElement>) {
173
173
  return (
174
174
  <span
175
- className={cn("ms-auto text-xs tracking-widest text-muted-foreground", className)}
175
+ className={cn("ms-auto text-meta tracking-widest text-muted-foreground", className)}
176
176
  {...props}
177
177
  />
178
178
  );
@@ -1,11 +1,37 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ import { dirname, join } from "node:path";
1
4
  import { describe, expect, it } from "vitest";
2
5
  import { render, screen } from "@testing-library/react";
3
6
  import { EmptyState } from "./empty-state";
4
7
 
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const source = readFileSync(join(__dirname, "empty-state.tsx"), "utf8");
10
+
5
11
  describe("EmptyState", () => {
6
12
  it("renders title and description", () => {
7
13
  render(<EmptyState title="Empty" description="Nothing here" />);
8
14
  expect(screen.getByText("Empty")).toBeInTheDocument();
9
15
  expect(screen.getByText("Nothing here")).toBeInTheDocument();
10
16
  });
17
+
18
+ it("clamps a passed icon to the fixed 40×40 slot, matching its documented behavior (#47)", () => {
19
+ // EmptyState only ever forwards `icon` to StatePanel, whose fixed-size
20
+ // icon slot clamps it via `[&_svg]:size-10` — it never forwards
21
+ // `illustration`, whose slot is unclamped. Lock the REAL behavior so it
22
+ // can't silently drift further from whatever the JSDoc claims.
23
+ render(<EmptyState title="X" icon={<svg data-testid="passed-icon" />} />);
24
+ const svg = screen.getByTestId("passed-icon");
25
+ expect(svg.parentElement?.className ?? "").toContain("size-10");
26
+ });
27
+
28
+ it("JSDoc claims only what `icon` actually does — does not promise an unclamped illustration slot (#47)", () => {
29
+ // #47 finding C: the JSDoc said "icon/illustration" but the component
30
+ // only ever forwards `icon` (still clamped to 40x40). A reader passing a
31
+ // larger illustration through `icon` expecting it to render at full size
32
+ // would be surprised by the clamp. The doc must describe the clamp, not
33
+ // the capability the deprecated wrapper doesn't have.
34
+ expect(source).not.toMatch(/icon\/illustration/i);
35
+ expect(source).toMatch(/clamp/i);
36
+ });
11
37
  });
@@ -8,7 +8,12 @@ import { StatePanel } from "../state-panel/state-panel";
8
8
  export interface EmptyStateProps {
9
9
  title: ReactNode;
10
10
  description?: ReactNode;
11
- /** Optional icon/illustration shown above the title. */
11
+ /**
12
+ * Optional icon shown above the title, clamped to a fixed 40×40 (this
13
+ * wrapper only ever forwards `icon` to `StatePanel`, never `illustration`
14
+ * — for a larger, unclamped illustration use
15
+ * `StatePanel kind="empty" illustration={...}` directly).
16
+ */
12
17
  icon?: ReactNode;
13
18
  /** Primary/secondary actions. */
14
19
  actions?: ReactNode;
@@ -111,8 +111,7 @@ export const expandDialogPanesVariants = cva("grid min-h-0 gap-3 overflow-hidden
111
111
  * and a pane holding only text has no focusable child to scroll from. Pass
112
112
  * `tabIndex={-1}` when the pane always contains its own focusable content.
113
113
  */
114
- const paneClass =
115
- "min-h-0 min-w-0 overflow-auto overscroll-contain focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
114
+ const paneClass = "min-h-0 min-w-0 overflow-auto overscroll-contain focus-ring";
116
115
 
117
116
  export interface ExpandDialogPanesProps
118
117
  extends
@@ -0,0 +1,59 @@
1
+ import { createContext, use } from "react";
2
+
3
+ /**
4
+ * Shared, lifted state for the `Field*` compound anatomy (`FieldRoot` +
5
+ * `FieldLabel`/`FieldControl`/`FieldDescription`/`FieldError`). `FieldRoot`
6
+ * is the only place this state is produced; every part reads the INTERFACE
7
+ * below rather than receiving positional props, so parts can be composed in
8
+ * any order/layout (compound-component + lifted-state convention — see
9
+ * `.claude/rules/component-api.md` "Composition patterns").
10
+ */
11
+ export interface FieldContextValue {
12
+ /**
13
+ * The id `FieldLabel`'s `htmlFor` points to — the FIRST `FieldControl`
14
+ * mounted under this `FieldRoot`, by JSX order (self-registered; see
15
+ * `registerControl`). `undefined` until a control has registered.
16
+ */
17
+ labelFor: string | undefined;
18
+ /** Registers a `FieldControl`'s resolved id (called while it is mounted). */
19
+ registerControl: (id: string) => void;
20
+ /** Unregisters a `FieldControl`'s id (called on unmount / when its id changes). */
21
+ unregisterControl: (id: string) => void;
22
+ /** `FieldRoot`'s `invalid` prop — drives `aria-invalid` on every `FieldControl` and error styling on `FieldLabel`. */
23
+ invalid: boolean;
24
+ /** `FieldRoot`'s `required` prop — drives `aria-required` on every `FieldControl`. */
25
+ required: boolean;
26
+ /**
27
+ * The composed `aria-describedby` value from every mounted `FieldDescription`/
28
+ * `FieldError` instance WITH content, descriptions first (in mount order),
29
+ * then errors (in mount order) — a fixed semantic order regardless of DOM
30
+ * position, so more than one of either part (or reordering them relative to
31
+ * `FieldControl`) never scrambles the announced order. `undefined` when
32
+ * nothing is registered.
33
+ */
34
+ describedBy: string | undefined;
35
+ /**
36
+ * Registers one `FieldDescription` INSTANCE's own id (called while it has
37
+ * content). Each instance generates and registers its own id — this is not
38
+ * a single shared slot, so more than one `FieldDescription` under one
39
+ * `FieldRoot` is supported without id collisions.
40
+ */
41
+ registerDescription: (id: string) => void;
42
+ /** Unregisters one `FieldDescription` instance's id (on unmount / when content is cleared). Removes only that instance's id. */
43
+ unregisterDescription: (id: string) => void;
44
+ /** Registers one `FieldError` instance's own id (called while it has content). Same per-instance contract as `registerDescription`. */
45
+ registerError: (id: string) => void;
46
+ /** Unregisters one `FieldError` instance's id (on unmount / when content is cleared). Removes only that instance's id. */
47
+ unregisterError: (id: string) => void;
48
+ }
49
+
50
+ export const FieldContext = createContext<FieldContextValue | null>(null);
51
+
52
+ /** Reads the shared `Field*` context; throws with the calling component's name when used outside a `FieldRoot`. */
53
+ export function useFieldContext(component: string): FieldContextValue {
54
+ const ctx = use(FieldContext);
55
+ if (!ctx) {
56
+ throw new Error(`<${component}> must be rendered inside a <FieldRoot>.`);
57
+ }
58
+ return ctx;
59
+ }
@@ -0,0 +1,142 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect } from "storybook/test";
3
+ import { Input } from "../input";
4
+ import { FieldRoot, FieldLabel, FieldControl, FieldDescription, FieldError } from "./field";
5
+
6
+ const meta = {
7
+ title: "Forms/Field",
8
+ component: FieldRoot,
9
+ tags: ["autodocs"],
10
+ parameters: {
11
+ docs: {
12
+ description: {
13
+ component:
14
+ "A compound field anatomy — `FieldRoot` + `FieldLabel`/`FieldControl`/`FieldDescription`/`FieldError` — for callers who need to COMPOSE a field layout `FieldRow` (Forms/FieldRow) can't express: more than one control in one row, or a description placed before the control. Every part reads shared context (`FieldRoot` owns id generation and `aria-describedby` composition), so they can be arranged in any order/layout. `FieldRow` remains the convenience wrapper for the common single-control case and is unaffected (#43).",
15
+ },
16
+ },
17
+ },
18
+ } satisfies Meta<typeof FieldRoot>;
19
+ export default meta;
20
+ type Story = StoryObj<typeof meta>;
21
+
22
+ export const Default: Story = {
23
+ render: () => (
24
+ <div className="w-72">
25
+ <FieldRoot>
26
+ <FieldLabel>Name</FieldLabel>
27
+ <FieldControl>
28
+ <Input placeholder="Jane Doe" />
29
+ </FieldControl>
30
+ </FieldRoot>
31
+ </div>
32
+ ),
33
+ play: async ({ canvas }) => {
34
+ const input = canvas.getByRole("textbox");
35
+ expect(input).not.toHaveAttribute("aria-describedby");
36
+ expect(input).toHaveAttribute("aria-invalid", "false");
37
+ expect(canvas.getByText("Name")).toHaveAttribute("for", input.id);
38
+ },
39
+ };
40
+
41
+ export const WithDescriptionAndError: Story = {
42
+ name: "With description and error",
43
+ render: () => (
44
+ <div className="w-72">
45
+ <FieldRoot invalid required>
46
+ <FieldLabel>API key</FieldLabel>
47
+ <FieldControl>
48
+ <Input placeholder="sk-…" />
49
+ </FieldControl>
50
+ <FieldDescription>Found in your account settings.</FieldDescription>
51
+ <FieldError>Enter a valid API key.</FieldError>
52
+ </FieldRoot>
53
+ </div>
54
+ ),
55
+ play: async ({ canvas }) => {
56
+ const input = canvas.getByRole("textbox");
57
+ expect(input).toHaveAttribute("aria-invalid", "true");
58
+ expect(input).toHaveAttribute("aria-required", "true");
59
+ const describedBy = input.getAttribute("aria-describedby")?.split(" ") ?? [];
60
+ expect(describedBy).toHaveLength(2);
61
+ expect(canvas.getByText("Found in your account settings.").id).toBe(describedBy[0]);
62
+ const alert = canvas.getByRole("alert");
63
+ expect(alert).toHaveTextContent("Enter a valid API key.");
64
+ expect(alert.id).toBe(describedBy[1]);
65
+ },
66
+ };
67
+
68
+ /**
69
+ * #43's first motivating case: `FieldRow`'s `children: ReactElement` can only
70
+ * ever wrap ONE control, so a first/last-name row (or a control plus an
71
+ * inline unit suffix) can't be expressed. Here two `FieldControl`s share one
72
+ * `FieldRoot` — each gets its own independent `id` (given explicitly, the
73
+ * same way any two form controls would need distinct ids) while both read
74
+ * the SAME `invalid`/`aria-describedby` wiring from context. `FieldLabel`'s
75
+ * `htmlFor` can only point to ONE control (the first), so each `Input` also
76
+ * carries its own `aria-label` — a shared visual label plus a placeholder is
77
+ * not enough of an accessible name for the second control (axe:
78
+ * `label-title-only`).
79
+ */
80
+ export const TwoControlsInOneRow: Story = {
81
+ name: "Two controls in one row",
82
+ render: () => (
83
+ <div className="w-96">
84
+ <FieldRoot invalid>
85
+ <FieldLabel>Name</FieldLabel>
86
+ <div className="flex gap-2">
87
+ <FieldControl>
88
+ <Input id="first-name" aria-label="First name" placeholder="First name" />
89
+ </FieldControl>
90
+ <FieldControl>
91
+ <Input id="last-name" aria-label="Last name" placeholder="Last name" />
92
+ </FieldControl>
93
+ </div>
94
+ <FieldError>Both first and last name are required.</FieldError>
95
+ </FieldRoot>
96
+ </div>
97
+ ),
98
+ play: async ({ canvas }) => {
99
+ const first = canvas.getByPlaceholderText("First name");
100
+ const last = canvas.getByPlaceholderText("Last name");
101
+ expect(first.id).toBe("first-name");
102
+ expect(last.id).toBe("last-name");
103
+ expect(first.id).not.toBe(last.id);
104
+
105
+ const alert = canvas.getByRole("alert");
106
+ expect(first).toHaveAttribute("aria-invalid", "true");
107
+ expect(last).toHaveAttribute("aria-invalid", "true");
108
+ expect(first.getAttribute("aria-describedby")).toBe(alert.id);
109
+ expect(last.getAttribute("aria-describedby")).toBe(alert.id);
110
+ },
111
+ };
112
+
113
+ /**
114
+ * #43's second motivating case: `FieldRow` always renders description AFTER
115
+ * the control. Here `FieldDescription` is placed BEFORE `FieldControl` in
116
+ * JSX — the visible order follows the JSX order, but `aria-describedby`
117
+ * wiring is unaffected either way (`FieldRoot` composes it from context, not
118
+ * DOM position).
119
+ */
120
+ export const DescriptionBeforeControl: Story = {
121
+ name: "Description between label and control",
122
+ render: () => (
123
+ <div className="w-72">
124
+ <FieldRoot>
125
+ <FieldLabel>Bio</FieldLabel>
126
+ <FieldDescription>Shown on your public profile.</FieldDescription>
127
+ <FieldControl>
128
+ <Input placeholder="Tell us about yourself…" />
129
+ </FieldControl>
130
+ </FieldRoot>
131
+ </div>
132
+ ),
133
+ play: async ({ canvas }) => {
134
+ const input = canvas.getByRole("textbox");
135
+ const description = canvas.getByText("Shown on your public profile.");
136
+ expect(input.getAttribute("aria-describedby")).toBe(description.id);
137
+ // The description renders before the control in the DOM, matching JSX order.
138
+ expect(
139
+ description.compareDocumentPosition(input) & Node.DOCUMENT_POSITION_FOLLOWING,
140
+ ).toBeTruthy();
141
+ },
142
+ };