@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
@@ -7,6 +7,7 @@ import {
7
7
  BUILT_IN_THEME_DEFINITIONS,
8
8
  defineTheme,
9
9
  ThemeProvider,
10
+ useTheme,
10
11
  } from "@elabs-ai/components-tokens";
11
12
 
12
13
  import { ThemeSwitcher } from "./theme-switcher";
@@ -251,3 +252,184 @@ describe("ThemeSwitcher — consumer themes (ADR 0029)", () => {
251
252
  );
252
253
  });
253
254
  });
255
+
256
+ describe("ThemeSwitcher — theme families (ADR 0036)", () => {
257
+ const oceanLight = defineTheme({
258
+ value: "ocean-light",
259
+ label: "Ocean light",
260
+ dark: false,
261
+ family: "ocean",
262
+ familyLabel: "Ocean",
263
+ });
264
+ const oceanDark = defineTheme({
265
+ value: "ocean-dark",
266
+ label: "Ocean dark",
267
+ dark: true,
268
+ family: "ocean",
269
+ });
270
+ const dusk = defineTheme({
271
+ value: "dusk",
272
+ label: "Dusk",
273
+ dark: true,
274
+ family: "dusk",
275
+ familyLabel: "Dusk",
276
+ });
277
+ const midnight = defineTheme({ value: "midnight", label: "Midnight", dark: true });
278
+ const daylight = defineTheme({ value: "daylight", label: "Daylight", dark: false });
279
+ const REGISTRY = [...BUILT_IN_THEME_DEFINITIONS, oceanLight, oceanDark, dusk];
280
+
281
+ async function openMenu() {
282
+ await userEvent.click(screen.getByRole("button", { name: "Theme" }));
283
+ return screen.findByRole("menu");
284
+ }
285
+
286
+ it("BACKWARD COMPAT: the default provider still renders the cycle toggle", () => {
287
+ setup(<ThemeSwitcher />);
288
+ expect(screen.getByRole("button", { name: /theme:/i })).toBeInTheDocument();
289
+ });
290
+
291
+ it("BACKWARD COMPAT: built-ins plus one undeclared theme stays a flat dropdown", async () => {
292
+ render(
293
+ <ThemeProvider themes={[...BUILT_IN_THEME_DEFINITIONS, midnight]}>
294
+ <ThemeSwitcher />
295
+ </ThemeProvider>,
296
+ );
297
+ const menu = await openMenu();
298
+ expect(within(menu).queryAllByRole("menuitemradio")).toHaveLength(0);
299
+ expect(within(menu).getByText("Midnight")).toBeInTheDocument();
300
+ });
301
+
302
+ it("BACKWARD COMPAT: an undeclared two-theme pair stays a light/dark toggle", () => {
303
+ render(
304
+ <ThemeProvider themes={[daylight, midnight]} defaultTheme="daylight">
305
+ <ThemeSwitcher showSystem={false} />
306
+ </ThemeProvider>,
307
+ );
308
+ expect(screen.getByRole("button", { name: /theme:/i })).toHaveAccessibleName(
309
+ "Theme: Daylight. Activate to switch to Midnight.",
310
+ );
311
+ });
312
+
313
+ it("offers families and a separate mode group once two families are declared", async () => {
314
+ render(
315
+ <ThemeProvider themes={REGISTRY} defaultTheme="light">
316
+ <ThemeSwitcher />
317
+ </ThemeProvider>,
318
+ );
319
+ const menu = await openMenu();
320
+ const radios = within(menu).getAllByRole("menuitemradio");
321
+ expect(radios.map((r) => r.textContent)).toEqual([
322
+ "Default",
323
+ "Ocean",
324
+ "Dusk",
325
+ "Light",
326
+ "Dark",
327
+ "System",
328
+ ]);
329
+ expect(within(menu).getByRole("menuitemradio", { name: "Default" })).toBeChecked();
330
+ expect(within(menu).getByRole("menuitemradio", { name: "Light" })).toBeChecked();
331
+ });
332
+
333
+ it("switching family keeps the chosen mode", async () => {
334
+ render(
335
+ <ThemeProvider themes={REGISTRY} defaultTheme="dark">
336
+ <ThemeSwitcher />
337
+ </ThemeProvider>,
338
+ );
339
+ await openMenu();
340
+ await userEvent.click(screen.getByRole("menuitemradio", { name: "Ocean" }));
341
+ expect(document.documentElement.getAttribute("data-theme")).toBe("ocean-dark");
342
+ });
343
+
344
+ it("switching mode stays within the family", async () => {
345
+ render(
346
+ <ThemeProvider themes={REGISTRY} defaultTheme="ocean-light">
347
+ <ThemeSwitcher />
348
+ </ThemeProvider>,
349
+ );
350
+ await openMenu();
351
+ await userEvent.click(screen.getByRole("menuitemradio", { name: "Dark" }));
352
+ expect(document.documentElement.getAttribute("data-theme")).toBe("ocean-dark");
353
+ });
354
+
355
+ it("hides the mode group for a single-mode family", async () => {
356
+ render(
357
+ <ThemeProvider themes={REGISTRY} defaultTheme="dusk">
358
+ <ThemeSwitcher />
359
+ </ThemeProvider>,
360
+ );
361
+ const menu = await openMenu();
362
+ expect(within(menu).queryByRole("menuitemradio", { name: "Light" })).not.toBeInTheDocument();
363
+ expect(within(menu).queryByText("Mode")).not.toBeInTheDocument();
364
+ });
365
+
366
+ it("remembers dark across a light-only family", async () => {
367
+ const sand = defineTheme({ value: "sand", label: "Sand", dark: false, family: "sand" });
368
+ render(
369
+ <ThemeProvider themes={[...REGISTRY, sand]} defaultTheme="dark">
370
+ <ThemeSwitcher />
371
+ </ThemeProvider>,
372
+ );
373
+ await openMenu();
374
+ await userEvent.click(screen.getByRole("menuitemradio", { name: "sand" }));
375
+ expect(document.documentElement.getAttribute("data-theme")).toBe("sand");
376
+ await openMenu();
377
+ await userEvent.click(screen.getByRole("menuitemradio", { name: "Ocean" }));
378
+ expect(document.documentElement.getAttribute("data-theme")).toBe("ocean-dark");
379
+ });
380
+
381
+ it("checks the right family when a second same-scheme variant is active", async () => {
382
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
383
+ const oceanDeep = defineTheme({
384
+ value: "ocean-deep",
385
+ label: "Ocean deep",
386
+ dark: true,
387
+ family: "ocean",
388
+ });
389
+ render(
390
+ <ThemeProvider themes={[...REGISTRY, oceanDeep]} defaultTheme="ocean-deep">
391
+ <ThemeSwitcher />
392
+ </ThemeProvider>,
393
+ );
394
+ const menu = await openMenu();
395
+ expect(within(menu).getByRole("menuitemradio", { name: "Ocean" })).toBeChecked();
396
+ expect(within(menu).getByRole("menuitemradio", { name: "Default" })).not.toBeChecked();
397
+ warn.mockRestore();
398
+ });
399
+
400
+ it("forgets a remembered mode once another control changes the theme", async () => {
401
+ function ExternalLight() {
402
+ const { setTheme } = useTheme();
403
+ return (
404
+ <button type="button" onClick={() => setTheme("light")}>
405
+ External light
406
+ </button>
407
+ );
408
+ }
409
+ render(
410
+ <ThemeProvider themes={REGISTRY} defaultTheme="light">
411
+ <ThemeSwitcher />
412
+ <ExternalLight />
413
+ </ThemeProvider>,
414
+ );
415
+ await openMenu();
416
+ await userEvent.click(screen.getByRole("menuitemradio", { name: "Dark" }));
417
+ expect(document.documentElement.getAttribute("data-theme")).toBe("dark");
418
+ await userEvent.click(screen.getByRole("button", { name: "External light" }));
419
+ expect(document.documentElement.getAttribute("data-theme")).toBe("light");
420
+ await openMenu();
421
+ await userEvent.click(screen.getByRole("menuitemradio", { name: "Ocean" }));
422
+ expect(document.documentElement.getAttribute("data-theme")).toBe("ocean-light");
423
+ });
424
+
425
+ it("forced dropdown mode keeps the flat list even with families", async () => {
426
+ render(
427
+ <ThemeProvider themes={REGISTRY}>
428
+ <ThemeSwitcher mode="dropdown" />
429
+ </ThemeProvider>,
430
+ );
431
+ const menu = await openMenu();
432
+ expect(within(menu).queryAllByRole("menuitemradio")).toHaveLength(0);
433
+ expect(within(menu).getByText("Ocean dark")).toBeInTheDocument();
434
+ });
435
+ });
@@ -1,17 +1,31 @@
1
1
  "use client";
2
2
 
3
- import { forwardRef, useCallback, useEffect, useState, type HTMLAttributes } from "react";
3
+ import { forwardRef, useCallback, useEffect, useRef, useState, type HTMLAttributes } from "react";
4
4
  import { Check, Monitor, Moon, Sun } from "lucide-react";
5
- import { useTheme, type ThemeDefinition, type ThemeName } from "@elabs-ai/components-tokens";
5
+ import {
6
+ groupThemeFamilies,
7
+ resolveThemeVariant,
8
+ themeFamilyIdOf,
9
+ themeSchemeOf,
10
+ useTheme,
11
+ type ThemeDefinition,
12
+ type ThemeName,
13
+ type ThemeScheme,
14
+ } from "@elabs-ai/components-tokens";
6
15
 
7
16
  import { Button } from "../button";
8
17
  import {
9
18
  DropdownMenu,
10
19
  DropdownMenuContent,
11
20
  DropdownMenuItem,
21
+ DropdownMenuLabel,
22
+ DropdownMenuRadioGroup,
23
+ DropdownMenuRadioItem,
24
+ DropdownMenuSeparator,
12
25
  DropdownMenuTrigger,
13
26
  } from "../dropdown-menu";
14
27
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../tooltip";
28
+ import { useLocale } from "../locale-provider";
15
29
  import { useThemeTransition, type ThemeTransitionEffect } from "./use-theme-transition";
16
30
 
17
31
  const SYSTEM_STORAGE_KEY = "brand-ui-theme-system";
@@ -107,6 +121,7 @@ export const ThemeSwitcher = forwardRef<HTMLButtonElement, ThemeSwitcherProps>(
107
121
  ref,
108
122
  ) {
109
123
  const { theme, themeDefinitions, setTheme } = useTheme();
124
+ const { t } = useLocale();
110
125
  const switchTheme = useThemeTransition(effect);
111
126
  const isControlled = preference !== undefined;
112
127
  const [uncontrolledIsSystem, setUncontrolledIsSystem] = useState(false);
@@ -127,16 +142,43 @@ export const ThemeSwitcher = forwardRef<HTMLButtonElement, ThemeSwitcherProps>(
127
142
  const labelOf = (name: ThemeName) => offered.find((d) => d.value === name)?.label ?? name;
128
143
  const isDark = (name: ThemeName) => offered.find((d) => d.value === name)?.dark ?? false;
129
144
 
130
- // `?? theme` (the active theme), never a "light"/"dark" literal: with an open
131
- // registry those names may not exist at all in this app.
132
- const lightTheme: ThemeName = pickByDarkness(offered, false) ?? theme;
133
- const darkTheme: ThemeName = pickByDarkness(offered, true) ?? theme;
134
-
135
145
  // The concrete theme to DISPLAY as current: the controlled preference when
136
146
  // it names one, otherwise the theme actually applied via the provider.
137
147
  const resolvedTheme: ThemeName =
138
148
  isControlled && preference !== undefined && preference !== "system" ? preference : theme;
139
149
 
150
+ // ADR 0036 — the family layout is OPT-IN: only when the offered list (not the
151
+ // provider's `families`, so the `themes` prop still narrows) holds two or
152
+ // more DECLARED families. An undeclared `[daylight, midnight]` pair keeps
153
+ // today's toggle byte-for-byte.
154
+ const families = groupThemeFamilies(offered);
155
+ const useFamilies = mode === "auto" && families.filter((f) => f.declared).length >= 2;
156
+ // Match by the active definition's declared family, not the retained
157
+ // light/dark slots: a second same-scheme variant is still reachable via
158
+ // `setTheme` but is not in either slot.
159
+ const activeDefinition = offered.find((d) => d.value === resolvedTheme);
160
+ const activeFamily = useFamilies
161
+ ? ((activeDefinition && families.find((f) => f.id === themeFamilyIdOf(activeDefinition))) ??
162
+ families[0])
163
+ : undefined;
164
+ // "System" is scoped to the active family in the family layout.
165
+ const scope: readonly ThemeDefinition[] = activeFamily
166
+ ? [activeFamily.light, activeFamily.dark].filter((d): d is ThemeDefinition => d !== undefined)
167
+ : offered;
168
+
169
+ // `?? theme` (the active theme), never a "light"/"dark" literal: with an open
170
+ // registry those names may not exist at all in this app.
171
+ const lightTheme: ThemeName = pickByDarkness(scope, false) ?? theme;
172
+ const darkTheme: ThemeName = pickByDarkness(scope, true) ?? theme;
173
+
174
+ // The scheme the user last chose, so dark → a light-only family → a
175
+ // two-scheme family lands on dark again (mirrors the provider's `setFamily`).
176
+ // Remembered together with the theme it produced: once something else changes
177
+ // the theme (another control calling `setTheme`), the memory no longer applies.
178
+ const intendedSchemeRef = useRef<{ scheme: ThemeScheme; theme: ThemeName } | undefined>(
179
+ undefined,
180
+ );
181
+
140
182
  // Hydrate the "system" choice from localStorage and apply it on mount —
141
183
  // uncontrolled mode only. Controlled mode's "system" state comes from the
142
184
  // `preference` prop and never touches this component's own storage key.
@@ -193,6 +235,90 @@ export const ThemeSwitcher = forwardRef<HTMLButtonElement, ThemeSwitcherProps>(
193
235
  }, [switchTheme, darkTheme, lightTheme, isControlled, onPreferenceChange]);
194
236
 
195
237
  const TriggerIcon = isSystem ? Monitor : isDark(resolvedTheme) ? Moon : Sun;
238
+
239
+ // ---- Family layout (≥2 declared families): family group + scheme group ----
240
+ if (activeFamily) {
241
+ const activeScheme = activeDefinition;
242
+ const remembered = intendedSchemeRef.current;
243
+ const intended =
244
+ remembered && remembered.theme === resolvedTheme
245
+ ? remembered.scheme
246
+ : activeScheme
247
+ ? themeSchemeOf(activeScheme)
248
+ : "light";
249
+ const pickFamily = (familyId: string) => {
250
+ // System re-resolves against the OS immediately on a family change
251
+ // instead of waiting for the next `change` event.
252
+ const scheme: ThemeScheme = isSystem ? (prefersDark() ? "dark" : "light") : intended;
253
+ const next = resolveThemeVariant(families, familyId, scheme);
254
+ if (next === undefined) return;
255
+ intendedSchemeRef.current = { scheme: intended, theme: next };
256
+ if (isSystem) switchTheme(next);
257
+ else pickTheme(next);
258
+ };
259
+ const pickScheme = (value: string) => {
260
+ if (value === "system") {
261
+ pickSystem();
262
+ return;
263
+ }
264
+ const scheme = value as ThemeScheme;
265
+ const next = activeFamily[scheme]?.value;
266
+ if (next === undefined) return;
267
+ intendedSchemeRef.current = { scheme, theme: next };
268
+ pickTheme(next);
269
+ };
270
+ const schemeValue = isSystem ? "system" : activeScheme ? themeSchemeOf(activeScheme) : "";
271
+
272
+ return (
273
+ <DropdownMenu>
274
+ <DropdownMenuTrigger asChild>
275
+ <Button
276
+ ref={ref}
277
+ variant="outline"
278
+ size={ICON_SIZE[size]}
279
+ aria-label={t("ui.themeSwitcher.theme")}
280
+ className={className}
281
+ {...props}
282
+ >
283
+ <TriggerIcon />
284
+ </Button>
285
+ </DropdownMenuTrigger>
286
+ <DropdownMenuContent align="end">
287
+ <DropdownMenuLabel>{t("ui.themeSwitcher.theme")}</DropdownMenuLabel>
288
+ <DropdownMenuRadioGroup value={activeFamily.id} onValueChange={pickFamily}>
289
+ {families.map((f) => (
290
+ <DropdownMenuRadioItem key={f.id} value={f.id}>
291
+ {f.label}
292
+ </DropdownMenuRadioItem>
293
+ ))}
294
+ </DropdownMenuRadioGroup>
295
+ {activeFamily.schemes.length === 2 ? (
296
+ <>
297
+ <DropdownMenuSeparator />
298
+ <DropdownMenuLabel>{t("ui.themeSwitcher.mode")}</DropdownMenuLabel>
299
+ <DropdownMenuRadioGroup value={schemeValue} onValueChange={pickScheme}>
300
+ <DropdownMenuRadioItem value="light">
301
+ <Sun aria-hidden="true" />
302
+ {t("ui.themeSwitcher.light")}
303
+ </DropdownMenuRadioItem>
304
+ <DropdownMenuRadioItem value="dark">
305
+ <Moon aria-hidden="true" />
306
+ {t("ui.themeSwitcher.dark")}
307
+ </DropdownMenuRadioItem>
308
+ {showSystem ? (
309
+ <DropdownMenuRadioItem value="system">
310
+ <Monitor aria-hidden="true" />
311
+ {t("ui.themeSwitcher.system")}
312
+ </DropdownMenuRadioItem>
313
+ ) : null}
314
+ </DropdownMenuRadioGroup>
315
+ </>
316
+ ) : null}
317
+ </DropdownMenuContent>
318
+ </DropdownMenu>
319
+ );
320
+ }
321
+
196
322
  const useDropdown = mode === "dropdown" || (mode === "auto" && safeThemes.length > 2);
197
323
 
198
324
  // ---- Dropdown mode (>2 themes) ----
@@ -204,7 +330,7 @@ export const ThemeSwitcher = forwardRef<HTMLButtonElement, ThemeSwitcherProps>(
204
330
  ref={ref}
205
331
  variant="outline"
206
332
  size={ICON_SIZE[size]}
207
- aria-label="Theme"
333
+ aria-label={t("ui.themeSwitcher.theme")}
208
334
  className={className}
209
335
  {...props}
210
336
  >
@@ -215,15 +341,15 @@ export const ThemeSwitcher = forwardRef<HTMLButtonElement, ThemeSwitcherProps>(
215
341
  {showSystem ? (
216
342
  <DropdownMenuItem onSelect={() => pickSystem()}>
217
343
  <Monitor />
218
- <span>System</span>
219
- {isSystem ? <Check className="ml-auto" /> : null}
344
+ <span>{t("ui.themeSwitcher.system")}</span>
345
+ {isSystem ? <Check className="ms-auto" /> : null}
220
346
  </DropdownMenuItem>
221
347
  ) : null}
222
348
  {offered.map((d) => (
223
349
  <DropdownMenuItem key={d.value} onSelect={() => pickTheme(d.value)}>
224
350
  {d.dark ? <Moon /> : <Sun />}
225
351
  <span>{d.label}</span>
226
- {!isSystem && resolvedTheme === d.value ? <Check className="ml-auto" /> : null}
352
+ {!isSystem && resolvedTheme === d.value ? <Check className="ms-auto" /> : null}
227
353
  </DropdownMenuItem>
228
354
  ))}
229
355
  </DropdownMenuContent>
@@ -254,7 +380,9 @@ export const ThemeSwitcher = forwardRef<HTMLButtonElement, ThemeSwitcherProps>(
254
380
  // `role` is optional only because `order[i]` is an indexed read under
255
381
  // `noUncheckedIndexedAccess`; the modulo above makes it always present.
256
382
  const roleLabel = (role: "light" | "dark" | "system" | undefined) =>
257
- role === "system" ? "System" : labelOf(role === "dark" ? darkTheme : lightTheme);
383
+ role === "system"
384
+ ? t("ui.themeSwitcher.system")
385
+ : labelOf(role === "dark" ? darkTheme : lightTheme);
258
386
 
259
387
  return (
260
388
  <TooltipProvider>
@@ -15,8 +15,8 @@ type ViewTransitionDocument = Document & {
15
15
  * Transitions API (the chanhdai.com "theme toggle effect", adapted to brand-ui).
16
16
  *
17
17
  * - Direction-aware: sets `data-vt="to-dark|to-light"` on `<html>` from the
18
- * target theme's `dark` flag, so the clip-path wipes the right way for ANY of
19
- * the three themes (not just light/dark).
18
+ * target theme's `dark` flag, so the clip-path wipes the right way for ANY
19
+ * theme (not just light/dark).
20
20
  * - `data-vt-effect` selects the reveal shape (default "polygon").
21
21
  * - Motion-aware: under `useReducedMotion()` (OS or in-app preference) — or when
22
22
  * the browser lacks `startViewTransition` — it falls back to an instant,
@@ -11,6 +11,9 @@ const meta = {
11
11
  docs: {
12
12
  description: {
13
13
  component:
14
+ "The GENERIC ordered-steps rail; an agent execution trace is `AI/AgentTimeline` " +
15
+ "and git history is `Data/RevisionTimeline` — see " +
16
+ "[Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs). " +
14
17
  "Ordered steps with a vertical connector and status-colored nodes — THE shared " +
15
18
  "rail spine (#190). Two front doors over one rail: the array `items` API (the " +
16
19
  "editor's `:::timeline` target, `done|active|pending`) and the compound " +
@@ -78,6 +81,30 @@ export const Compound: Story = {
78
81
  },
79
82
  };
80
83
 
84
+ export const TabularTimestamps: Story = {
85
+ // #386 — a right-aligned timestamp column of differing digit widths
86
+ // ("09:14" vs "11:47" vs "9:02") must not jitter: the span needs
87
+ // `tabular-nums` so every digit occupies equal width.
88
+ args: { items: [] },
89
+ render: () => (
90
+ <TimelineRoot aria-label="Run with numeric timestamps">
91
+ <TimelineItem status="complete" timestamp="09:14">
92
+ Searched financial filings
93
+ </TimelineItem>
94
+ <TimelineItem status="complete" timestamp="11:47">
95
+ Queried finance.revenue
96
+ </TimelineItem>
97
+ <TimelineItem status="pending" timestamp="9:02">
98
+ Draft the board note
99
+ </TimelineItem>
100
+ </TimelineRoot>
101
+ ),
102
+ play: async ({ canvas }) => {
103
+ const timestamp = await waitFor(() => canvas.getByText("09:14"));
104
+ await expect(getComputedStyle(timestamp).fontVariantNumeric).toContain("tabular-nums");
105
+ },
106
+ };
107
+
81
108
  export const CanonicalStatuses: Story = {
82
109
  args: { items: [] },
83
110
  render: () => (
@@ -15,8 +15,15 @@
15
15
  * parts. Keeps the editor-facing `done|active|pending` API unchanged by
16
16
  * mapping in via `fromTimelineStatus` at the boundary.
17
17
  *
18
- * Fork-prevention: a Timeline re-implementation outside this folder fails
19
- * `pnpm timeline-fork:check` (scripts/check-timeline-fork.mjs).
18
+ * Deliberately NOT on this rail (a different grammar, not an oversight
19
+ * RM-014, #133): `RevisionTimeline` (@elabs-ai/components-ui). Its rail is a
20
+ * multi-lane commit DAG whose node colour is LANE IDENTITY (the chart ramp),
21
+ * not `Status`, and whose fork/merge joins are cross-row beziers living in one
22
+ * SVG coordinate space spanning the whole list — neither of which a
23
+ * status-keyed node or an item-local `w-px` connector can express. Carrying
24
+ * both grammars here would cost ~5 opt-in props, four of them switching this
25
+ * rail's own node/connector/geometry/status announcement off. The four-point
26
+ * reason is in that component's docblock; reopen #133 before converging them.
20
27
  */
21
28
  import { forwardRef, type HTMLAttributes, type LiHTMLAttributes, type ReactNode } from "react";
22
29
  import { cn } from "../../lib/cn";
@@ -141,7 +148,7 @@ export const TimelineItem = forwardRef<HTMLLIElement, TimelineItemProps>(functio
141
148
  {children}
142
149
  </span>
143
150
  {timestamp ? (
144
- <span className="shrink-0 text-meta text-muted-foreground">{timestamp}</span>
151
+ <span className="shrink-0 text-meta text-muted-foreground tabular-nums">{timestamp}</span>
145
152
  ) : null}
146
153
  </div>
147
154
  {description ? <p className="mt-0.5 text-body text-muted-foreground">{description}</p> : null}
@@ -21,7 +21,7 @@ export const toggleVariants = cva(
21
21
  // asChild wrapper overwrites data-state="on" with "closed" — aria-pressed
22
22
  // (Toggle) and aria-checked (ToggleGroupItem) are set by the toggle primitive
23
23
  // alone and survive the merge. (#214)
24
- "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors duration-fast ease-standard hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground data-[state=on]:font-semibold aria-pressed:bg-accent aria-pressed:text-accent-foreground aria-pressed:font-semibold aria-checked:bg-accent aria-checked:text-accent-foreground aria-checked:font-semibold [&_svg]:size-4 [&_svg]:shrink-0",
24
+ "inline-flex items-center justify-center gap-2 rounded-control text-body font-control transition-colors duration-fast ease-standard hover:bg-muted hover:text-muted-foreground focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground data-[state=on]:font-semibold aria-pressed:bg-accent aria-pressed:text-accent-foreground aria-pressed:font-semibold aria-checked:bg-accent aria-checked:text-accent-foreground aria-checked:font-semibold [&_svg]:size-4 [&_svg]:shrink-0",
25
25
  {
26
26
  variants: {
27
27
  variant: {
@@ -44,7 +44,11 @@ export const toggleVariants = cva(
44
44
  segmented:
45
45
  "border-0 bg-transparent text-muted-foreground hover:bg-transparent hover:text-foreground aria-checked:bg-surface-elevated aria-checked:text-foreground aria-checked:font-medium aria-checked:shadow-sm aria-pressed:bg-surface-elevated aria-pressed:text-foreground aria-pressed:font-medium aria-pressed:shadow-sm data-[state=on]:bg-surface-elevated data-[state=on]:text-foreground data-[state=on]:font-medium data-[state=on]:shadow-sm",
46
46
  },
47
- size: { default: "h-9 px-2.5 min-w-9", sm: "h-8 px-2 min-w-8", lg: "h-10 px-3 min-w-10" },
47
+ size: {
48
+ default: "h-control px-2.5 min-w-control",
49
+ sm: "h-control-sm px-2 min-w-control-sm",
50
+ lg: "h-control-lg px-3 min-w-control-lg",
51
+ },
48
52
  },
49
53
  defaultVariants: { variant: "default", size: "default" },
50
54
  },
@@ -57,6 +61,7 @@ export const Toggle = forwardRef<
57
61
  return (
58
62
  <TogglePrimitive.Root
59
63
  ref={ref}
64
+ data-slot="toggle"
60
65
  className={cn(toggleVariants({ variant, size }), className)}
61
66
  {...props}
62
67
  />
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import {
2
4
  createContext,
3
5
  forwardRef,
@@ -28,10 +28,18 @@ import {
28
28
  * expect there.
29
29
  */
30
30
  const meta = {
31
- title: "Foundation/Toolbar",
31
+ title: "Layout/Toolbar",
32
32
  component: Toolbar,
33
33
  tags: ["autodocs"],
34
- parameters: { layout: "padded" },
34
+ parameters: {
35
+ layout: "padded",
36
+ docs: {
37
+ description: {
38
+ component:
39
+ "The one row that genuinely claims the `toolbar` ARIA role: Radix delegates a roving tabindex to it, so ten controls cost one tab stop instead of ten. The row above a list, table or board — filters left, actions right — is `Layout/ViewToolbar`, which deliberately does NOT claim that role because its controls are ordinary tab stops. See [Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs).",
40
+ },
41
+ },
42
+ },
35
43
  args: { "aria-label": "Formatting" },
36
44
  } satisfies Meta<typeof Toolbar>;
37
45
 
@@ -14,9 +14,10 @@ export const TooltipContent = forwardRef<
14
14
  <TooltipPrimitive.Portal>
15
15
  <TooltipPrimitive.Content
16
16
  ref={ref}
17
+ data-slot="tooltip-content"
17
18
  sideOffset={sideOffset}
18
19
  className={cn(
19
- "z-50 overflow-hidden rounded-md bg-foreground px-2.5 py-1.5 text-xs text-background shadow-md",
20
+ "z-50 overflow-hidden rounded-md bg-foreground px-2.5 py-1.5 text-meta text-background shadow-md",
20
21
  "data-[state=delayed-open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95",
21
22
  className,
22
23
  )}
@@ -17,7 +17,7 @@ export const TopNav = forwardRef<HTMLElement, TopNavProps>(function TopNav(
17
17
  <header
18
18
  ref={ref}
19
19
  className={cn(
20
- "sticky top-0 z-30 flex h-14 items-center gap-4 border-b bg-surface-elevated/80 px-4 backdrop-blur",
20
+ "sticky top-0 z-30 flex h-header items-center gap-4 border-b bg-surface-elevated/80 px-4 backdrop-blur",
21
21
  className,
22
22
  )}
23
23
  {...props}