@teamofsilicons/silicon-ui 0.0.0-stage → 0.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 (354) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +81 -2
  3. package/check.mjs +236 -0
  4. package/components/accordion/accordion.d.ts +13 -0
  5. package/components/accordion/accordion.js +35 -0
  6. package/components/accordion/accordion.module.css +15 -0
  7. package/components/action-button/action-button.d.ts +11 -0
  8. package/components/action-button/action-button.js +57 -0
  9. package/components/action-button/action-button.module.css +15 -0
  10. package/components/activity-heatmap/activity-heatmap.d.ts +37 -0
  11. package/components/activity-heatmap/activity-heatmap.js +303 -0
  12. package/components/activity-heatmap/activity-heatmap.module.css +83 -0
  13. package/components/alert/alert.d.ts +13 -0
  14. package/components/alert/alert.js +79 -0
  15. package/components/alert/alert.module.css +15 -0
  16. package/components/animated-counter/animated-counter.d.ts +13 -0
  17. package/components/animated-counter/animated-counter.js +73 -0
  18. package/components/animated-counter/animated-counter.module.css +12 -0
  19. package/components/announcement-bar/announcement-bar.d.ts +53 -0
  20. package/components/announcement-bar/announcement-bar.js +187 -0
  21. package/components/announcement-bar/announcement-bar.module.css +60 -0
  22. package/components/avatar/avatar.d.ts +8 -0
  23. package/components/avatar/avatar.js +18 -0
  24. package/components/avatar/avatar.module.css +60 -0
  25. package/components/avatar-group/avatar-group.d.ts +13 -0
  26. package/components/avatar-group/avatar-group.js +24 -0
  27. package/components/avatar-group/avatar-group.module.css +27 -0
  28. package/components/badge/badge.d.ts +10 -0
  29. package/components/badge/badge.js +66 -0
  30. package/components/badge/badge.module.css +52 -0
  31. package/components/bar-chart/bar-chart.d.ts +32 -0
  32. package/components/bar-chart/bar-chart.js +327 -0
  33. package/components/bar-chart/bar-chart.module.css +49 -0
  34. package/components/billing-toggle/billing-toggle.d.ts +35 -0
  35. package/components/billing-toggle/billing-toggle.js +83 -0
  36. package/components/billing-toggle/billing-toggle.module.css +139 -0
  37. package/components/bottom-sheet/bottom-sheet.d.ts +27 -0
  38. package/components/bottom-sheet/bottom-sheet.js +386 -0
  39. package/components/bottom-sheet/bottom-sheet.module.css +69 -0
  40. package/components/breadcrumb/breadcrumb.d.ts +13 -0
  41. package/components/breadcrumb/breadcrumb.js +19 -0
  42. package/components/breadcrumb/breadcrumb.module.css +12 -0
  43. package/components/brush-chart/brush-chart.d.ts +45 -0
  44. package/components/brush-chart/brush-chart.js +426 -0
  45. package/components/brush-chart/brush-chart.module.css +69 -0
  46. package/components/button/button.d.ts +10 -0
  47. package/components/button/button.js +97 -0
  48. package/components/button/button.module.css +39 -0
  49. package/components/button-group/button-group.d.ts +59 -0
  50. package/components/button-group/button-group.js +280 -0
  51. package/components/button-group/button-group.module.css +87 -0
  52. package/components/button-link/button-link.d.ts +24 -0
  53. package/components/button-link/button-link.js +11 -0
  54. package/components/button-link/button-link.module.css +7 -0
  55. package/components/calendar/calendar.d.ts +22 -0
  56. package/components/calendar/calendar.js +253 -0
  57. package/components/calendar/calendar.module.css +51 -0
  58. package/components/card/card.d.ts +19 -0
  59. package/components/card/card.js +108 -0
  60. package/components/card/card.module.css +51 -0
  61. package/components/card-stack/card-stack.d.ts +34 -0
  62. package/components/card-stack/card-stack.js +291 -0
  63. package/components/card-stack/card-stack.module.css +63 -0
  64. package/components/carousel/carousel.d.ts +28 -0
  65. package/components/carousel/carousel.js +332 -0
  66. package/components/carousel/carousel.module.css +42 -0
  67. package/components/chat-thread/chat-thread.d.ts +135 -0
  68. package/components/chat-thread/chat-thread.js +406 -0
  69. package/components/chat-thread/chat-thread.module.css +242 -0
  70. package/components/checkbox/checkbox.d.ts +7 -0
  71. package/components/checkbox/checkbox.js +23 -0
  72. package/components/checkbox/checkbox.module.css +15 -0
  73. package/components/chip-group/chip-group.d.ts +24 -0
  74. package/components/chip-group/chip-group.js +196 -0
  75. package/components/chip-group/chip-group.module.css +38 -0
  76. package/components/code-block/code-block.d.ts +11 -0
  77. package/components/code-block/code-block.js +133 -0
  78. package/components/code-block/code-block.module.css +43 -0
  79. package/components/color-picker/color-picker.d.ts +40 -0
  80. package/components/color-picker/color-picker.js +449 -0
  81. package/components/color-picker/color-picker.module.css +111 -0
  82. package/components/combobox/combobox.d.ts +19 -0
  83. package/components/combobox/combobox.js +167 -0
  84. package/components/combobox/combobox.module.css +28 -0
  85. package/components/command-palette/command-palette.d.ts +20 -0
  86. package/components/command-palette/command-palette.js +151 -0
  87. package/components/command-palette/command-palette.module.css +94 -0
  88. package/components/comment-thread/comment-thread.d.ts +120 -0
  89. package/components/comment-thread/comment-thread.js +343 -0
  90. package/components/comment-thread/comment-thread.module.css +195 -0
  91. package/components/confirm-morph/confirm-morph.d.ts +65 -0
  92. package/components/confirm-morph/confirm-morph.js +355 -0
  93. package/components/confirm-morph/confirm-morph.module.css +146 -0
  94. package/components/context-menu/context-menu.d.ts +18 -0
  95. package/components/context-menu/context-menu.js +148 -0
  96. package/components/context-menu/context-menu.module.css +17 -0
  97. package/components/copy-button/copy-button.d.ts +10 -0
  98. package/components/copy-button/copy-button.js +69 -0
  99. package/components/copy-button/copy-button.module.css +21 -0
  100. package/components/countdown/countdown.d.ts +31 -0
  101. package/components/countdown/countdown.js +177 -0
  102. package/components/countdown/countdown.module.css +49 -0
  103. package/components/date-picker/date-picker.d.ts +17 -0
  104. package/components/date-picker/date-picker.js +100 -0
  105. package/components/date-picker/date-picker.module.css +34 -0
  106. package/components/date-range-picker/date-range-picker.d.ts +44 -0
  107. package/components/date-range-picker/date-range-picker.js +534 -0
  108. package/components/date-range-picker/date-range-picker.module.css +214 -0
  109. package/components/dialog/dialog.d.ts +11 -0
  110. package/components/dialog/dialog.js +52 -0
  111. package/components/dialog/dialog.module.css +36 -0
  112. package/components/donut-chart/donut-chart.d.ts +46 -0
  113. package/components/donut-chart/donut-chart.js +382 -0
  114. package/components/donut-chart/donut-chart.module.css +48 -0
  115. package/components/drawer/drawer.d.ts +20 -0
  116. package/components/drawer/drawer.js +111 -0
  117. package/components/drawer/drawer.module.css +97 -0
  118. package/components/dropdown-menu/dropdown-menu.d.ts +16 -0
  119. package/components/dropdown-menu/dropdown-menu.js +39 -0
  120. package/components/dropdown-menu/dropdown-menu.module.css +38 -0
  121. package/components/elastic-slider/elastic-slider.d.ts +39 -0
  122. package/components/elastic-slider/elastic-slider.js +261 -0
  123. package/components/elastic-slider/elastic-slider.module.css +40 -0
  124. package/components/empty-state/empty-state.d.ts +12 -0
  125. package/components/empty-state/empty-state.js +69 -0
  126. package/components/empty-state/empty-state.module.css +13 -0
  127. package/components/expandable-card/expandable-card.d.ts +13 -0
  128. package/components/expandable-card/expandable-card.js +89 -0
  129. package/components/expandable-card/expandable-card.module.css +22 -0
  130. package/components/expanding-button-group/expanding-button-group.d.ts +41 -0
  131. package/components/expanding-button-group/expanding-button-group.js +258 -0
  132. package/components/expanding-button-group/expanding-button-group.module.css +44 -0
  133. package/components/expanding-search/expanding-search.d.ts +38 -0
  134. package/components/expanding-search/expanding-search.js +242 -0
  135. package/components/expanding-search/expanding-search.module.css +52 -0
  136. package/components/file-dropzone/file-dropzone.d.ts +44 -0
  137. package/components/file-dropzone/file-dropzone.js +379 -0
  138. package/components/file-dropzone/file-dropzone.module.css +115 -0
  139. package/components/file-upload/file-upload.d.ts +22 -0
  140. package/components/file-upload/file-upload.js +192 -0
  141. package/components/file-upload/file-upload.module.css +49 -0
  142. package/components/filter-toolbar/filter-toolbar.d.ts +48 -0
  143. package/components/filter-toolbar/filter-toolbar.js +446 -0
  144. package/components/filter-toolbar/filter-toolbar.module.css +70 -0
  145. package/components/floating-button-group/floating-button-group.d.ts +51 -0
  146. package/components/floating-button-group/floating-button-group.js +215 -0
  147. package/components/floating-button-group/floating-button-group.module.css +74 -0
  148. package/components/gauge/gauge.d.ts +18 -0
  149. package/components/gauge/gauge.js +101 -0
  150. package/components/gauge/gauge.module.css +22 -0
  151. package/components/hold-to-confirm/hold-to-confirm.d.ts +25 -0
  152. package/components/hold-to-confirm/hold-to-confirm.js +165 -0
  153. package/components/hold-to-confirm/hold-to-confirm.module.css +20 -0
  154. package/components/hover-card/hover-card.d.ts +38 -0
  155. package/components/hover-card/hover-card.js +145 -0
  156. package/components/hover-card/hover-card.module.css +20 -0
  157. package/components/image-compare/image-compare.d.ts +27 -0
  158. package/components/image-compare/image-compare.js +267 -0
  159. package/components/image-compare/image-compare.module.css +25 -0
  160. package/components/in-view-title/in-view-title.d.ts +17 -0
  161. package/components/in-view-title/in-view-title.js +70 -0
  162. package/components/in-view-title/in-view-title.module.css +13 -0
  163. package/components/inline-edit/inline-edit.d.ts +27 -0
  164. package/components/inline-edit/inline-edit.js +315 -0
  165. package/components/inline-edit/inline-edit.module.css +73 -0
  166. package/components/input/input.d.ts +7 -0
  167. package/components/input/input.js +60 -0
  168. package/components/input/input.module.css +22 -0
  169. package/components/json-viewer/json-viewer.d.ts +48 -0
  170. package/components/json-viewer/json-viewer.js +463 -0
  171. package/components/json-viewer/json-viewer.module.css +92 -0
  172. package/components/line-chart/line-chart.d.ts +58 -0
  173. package/components/line-chart/line-chart.js +383 -0
  174. package/components/line-chart/line-chart.module.css +60 -0
  175. package/components/mention-input/mention-input.d.ts +72 -0
  176. package/components/mention-input/mention-input.js +417 -0
  177. package/components/mention-input/mention-input.module.css +134 -0
  178. package/components/metric-card/metric-card.d.ts +8 -0
  179. package/components/metric-card/metric-card.js +44 -0
  180. package/components/metric-card/metric-card.module.css +18 -0
  181. package/components/money-input/money-input.d.ts +48 -0
  182. package/components/money-input/money-input.js +503 -0
  183. package/components/money-input/money-input.module.css +77 -0
  184. package/components/morph-select/morph-select.d.ts +50 -0
  185. package/components/morph-select/morph-select.js +395 -0
  186. package/components/morph-select/morph-select.module.css +88 -0
  187. package/components/multi-select/multi-select.d.ts +18 -0
  188. package/components/multi-select/multi-select.js +105 -0
  189. package/components/multi-select/multi-select.module.css +37 -0
  190. package/components/notification-center/notification-center.d.ts +24 -0
  191. package/components/notification-center/notification-center.js +146 -0
  192. package/components/notification-center/notification-center.module.css +79 -0
  193. package/components/number-field/number-field.d.ts +41 -0
  194. package/components/number-field/number-field.js +455 -0
  195. package/components/number-field/number-field.module.css +71 -0
  196. package/components/otp-input/otp-input.d.ts +13 -0
  197. package/components/otp-input/otp-input.js +129 -0
  198. package/components/otp-input/otp-input.module.css +28 -0
  199. package/components/pagination/pagination.d.ts +7 -0
  200. package/components/pagination/pagination.js +60 -0
  201. package/components/pagination/pagination.module.css +16 -0
  202. package/components/password-field/password-field.d.ts +14 -0
  203. package/components/password-field/password-field.js +51 -0
  204. package/components/password-field/password-field.module.css +27 -0
  205. package/components/password-strength/password-strength.d.ts +107 -0
  206. package/components/password-strength/password-strength.js +161 -0
  207. package/components/password-strength/password-strength.module.css +57 -0
  208. package/components/phone-input/phone-input.d.ts +73 -0
  209. package/components/phone-input/phone-input.js +612 -0
  210. package/components/phone-input/phone-input.module.css +98 -0
  211. package/components/popover/popover.d.ts +6 -0
  212. package/components/popover/popover.js +19 -0
  213. package/components/popover/popover.module.css +32 -0
  214. package/components/progress/progress.d.ts +8 -0
  215. package/components/progress/progress.js +42 -0
  216. package/components/progress/progress.module.css +1 -0
  217. package/components/radio-cards/radio-cards.d.ts +35 -0
  218. package/components/radio-cards/radio-cards.js +110 -0
  219. package/components/radio-cards/radio-cards.module.css +84 -0
  220. package/components/radio-group/radio-group.d.ts +13 -0
  221. package/components/radio-group/radio-group.js +55 -0
  222. package/components/radio-group/radio-group.module.css +30 -0
  223. package/components/resizable-panels/resizable-panels.d.ts +37 -0
  224. package/components/resizable-panels/resizable-panels.js +338 -0
  225. package/components/resizable-panels/resizable-panels.module.css +40 -0
  226. package/components/rich-text-editor/rich-text-editor.d.ts +55 -0
  227. package/components/rich-text-editor/rich-text-editor.js +1733 -0
  228. package/components/rich-text-editor/rich-text-editor.module.css +201 -0
  229. package/components/ridgeline/ridgeline.d.ts +37 -0
  230. package/components/ridgeline/ridgeline.js +346 -0
  231. package/components/ridgeline/ridgeline.module.css +58 -0
  232. package/components/scroll-area/scroll-area.d.ts +40 -0
  233. package/components/scroll-area/scroll-area.js +153 -0
  234. package/components/scroll-area/scroll-area.module.css +79 -0
  235. package/components/search-field/search-field.d.ts +7 -0
  236. package/components/search-field/search-field.js +20 -0
  237. package/components/search-field/search-field.module.css +17 -0
  238. package/components/segmented-control/segmented-control.d.ts +16 -0
  239. package/components/segmented-control/segmented-control.js +56 -0
  240. package/components/segmented-control/segmented-control.module.css +15 -0
  241. package/components/select/select.d.ts +15 -0
  242. package/components/select/select.js +36 -0
  243. package/components/select/select.module.css +35 -0
  244. package/components/shortcut-recorder/shortcut-recorder.d.ts +108 -0
  245. package/components/shortcut-recorder/shortcut-recorder.js +380 -0
  246. package/components/shortcut-recorder/shortcut-recorder.module.css +85 -0
  247. package/components/signature-pad/signature-pad.d.ts +72 -0
  248. package/components/signature-pad/signature-pad.js +450 -0
  249. package/components/signature-pad/signature-pad.module.css +84 -0
  250. package/components/skeleton/skeleton.d.ts +13 -0
  251. package/components/skeleton/skeleton.js +53 -0
  252. package/components/skeleton/skeleton.module.css +11 -0
  253. package/components/slider/slider.d.ts +45 -0
  254. package/components/slider/slider.js +347 -0
  255. package/components/slider/slider.module.css +45 -0
  256. package/components/slope-chart/slope-chart.d.ts +38 -0
  257. package/components/slope-chart/slope-chart.js +183 -0
  258. package/components/slope-chart/slope-chart.module.css +51 -0
  259. package/components/slot-text/slot-text.d.ts +26 -0
  260. package/components/slot-text/slot-text.js +119 -0
  261. package/components/slot-text/slot-text.module.css +19 -0
  262. package/components/sortable-data-table/sortable-data-table.d.ts +38 -0
  263. package/components/sortable-data-table/sortable-data-table.js +240 -0
  264. package/components/sortable-data-table/sortable-data-table.module.css +116 -0
  265. package/components/sparkline/sparkline.d.ts +19 -0
  266. package/components/sparkline/sparkline.js +239 -0
  267. package/components/sparkline/sparkline.module.css +26 -0
  268. package/components/split-button/split-button.d.ts +19 -0
  269. package/components/split-button/split-button.js +33 -0
  270. package/components/split-button/split-button.module.css +51 -0
  271. package/components/stepper/stepper.d.ts +36 -0
  272. package/components/stepper/stepper.js +108 -0
  273. package/components/stepper/stepper.module.css +74 -0
  274. package/components/streamgraph/streamgraph.d.ts +51 -0
  275. package/components/streamgraph/streamgraph.js +376 -0
  276. package/components/streamgraph/streamgraph.module.css +55 -0
  277. package/components/swipe-actions/swipe-actions.d.ts +35 -0
  278. package/components/swipe-actions/swipe-actions.js +330 -0
  279. package/components/swipe-actions/swipe-actions.module.css +64 -0
  280. package/components/switch/switch.d.ts +7 -0
  281. package/components/switch/switch.js +45 -0
  282. package/components/switch/switch.module.css +12 -0
  283. package/components/tabs/tabs.d.ts +8 -0
  284. package/components/tabs/tabs.js +138 -0
  285. package/components/tabs/tabs.module.css +27 -0
  286. package/components/tag-input/tag-input.d.ts +15 -0
  287. package/components/tag-input/tag-input.js +150 -0
  288. package/components/tag-input/tag-input.module.css +34 -0
  289. package/components/text-morph/text-morph.d.ts +13 -0
  290. package/components/text-morph/text-morph.js +69 -0
  291. package/components/text-morph/text-morph.module.css +8 -0
  292. package/components/text-reveal/text-reveal.d.ts +14 -0
  293. package/components/text-reveal/text-reveal.js +20 -0
  294. package/components/text-reveal/text-reveal.module.css +13 -0
  295. package/components/text-shimmer/text-shimmer.d.ts +18 -0
  296. package/components/text-shimmer/text-shimmer.js +90 -0
  297. package/components/text-shimmer/text-shimmer.module.css +9 -0
  298. package/components/textarea/textarea.d.ts +7 -0
  299. package/components/textarea/textarea.js +56 -0
  300. package/components/textarea/textarea.module.css +22 -0
  301. package/components/theme-switch/theme-switch.d.ts +11 -0
  302. package/components/theme-switch/theme-switch.js +37 -0
  303. package/components/theme-switch/theme-switch.module.css +31 -0
  304. package/components/time-picker/time-picker.d.ts +13 -0
  305. package/components/time-picker/time-picker.js +95 -0
  306. package/components/time-picker/time-picker.module.css +23 -0
  307. package/components/timeline/timeline.d.ts +47 -0
  308. package/components/timeline/timeline.js +171 -0
  309. package/components/timeline/timeline.module.css +51 -0
  310. package/components/toast/toast.d.ts +8 -0
  311. package/components/toast/toast.js +118 -0
  312. package/components/toast/toast.module.css +100 -0
  313. package/components/toast-stack/toast-stack.d.ts +57 -0
  314. package/components/toast-stack/toast-stack.js +399 -0
  315. package/components/toast-stack/toast-stack.module.css +61 -0
  316. package/components/tooltip/tooltip.d.ts +8 -0
  317. package/components/tooltip/tooltip.js +62 -0
  318. package/components/tooltip/tooltip.module.css +18 -0
  319. package/components/tree-view/tree-view.d.ts +17 -0
  320. package/components/tree-view/tree-view.js +147 -0
  321. package/components/tree-view/tree-view.module.css +22 -0
  322. package/components/treemap/treemap.d.ts +35 -0
  323. package/components/treemap/treemap.js +375 -0
  324. package/components/treemap/treemap.module.css +59 -0
  325. package/components/usage-meter/usage-meter.d.ts +24 -0
  326. package/components/usage-meter/usage-meter.js +296 -0
  327. package/components/usage-meter/usage-meter.module.css +61 -0
  328. package/components/user-menu/user-menu.d.ts +62 -0
  329. package/components/user-menu/user-menu.js +316 -0
  330. package/components/user-menu/user-menu.module.css +97 -0
  331. package/components/waffle-chart/waffle-chart.d.ts +39 -0
  332. package/components/waffle-chart/waffle-chart.js +244 -0
  333. package/components/waffle-chart/waffle-chart.module.css +52 -0
  334. package/components.json +1330 -0
  335. package/eslint.js +53 -0
  336. package/lib/escape.d.ts +19 -0
  337. package/lib/escape.js +50 -0
  338. package/lib/media.d.ts +353 -0
  339. package/lib/media.js +65 -0
  340. package/lib/menu-highlight.d.ts +28 -0
  341. package/lib/menu-highlight.js +43 -0
  342. package/lib/motion-tokens.d.ts +60 -0
  343. package/lib/motion-tokens.js +25 -0
  344. package/lib/return-focus.d.ts +9 -0
  345. package/lib/return-focus.js +53 -0
  346. package/lib/silicon-provider.d.ts +60 -0
  347. package/lib/silicon-provider.js +47 -0
  348. package/lib/use-copy-feedback.d.ts +8 -0
  349. package/lib/use-copy-feedback.js +36 -0
  350. package/lib/use-today.d.ts +2 -0
  351. package/lib/use-today.js +29 -0
  352. package/package.json +76 -4
  353. package/styles.css +218 -0
  354. package/styles.css.d.ts +1 -0
@@ -0,0 +1,39 @@
1
+ /* Press scale is driven by Motion (whileTap) alone, so transform never gets a CSS transition here. */
2
+ .button { position: relative; display: inline-flex; min-height: var(--control-height-md); align-items: center; justify-content: center; gap: var(--space-related); border: 1px solid transparent; border-radius: var(--radius-control); corner-shape: var(--corner-shape); padding: 0 var(--space-inset); font: inherit; font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); white-space: nowrap; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: color var(--duration-fast) var(--ease-standard), background var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); }
3
+ .button:disabled { cursor: not-allowed; opacity: .52; }
4
+ .button[aria-busy="true"] { cursor: progress; }
5
+ /* Anchors of floating layers never scale: Radix opens on pointerdown and would measure a shrunken trigger. */
6
+ .button:is([aria-haspopup]:not([aria-haspopup="false"]), [data-state], [role="combobox"]) { transform: none !important; }
7
+ /* The slot's width follows the incoming label on a spring. The label stays centered and may overflow evenly while the slot catches up;
8
+ the outgoing label pops out of flow against the slot itself, so it keeps its place instead of following the new content box. */
9
+ .labelSlot { position: relative; display: inline-flex; min-width: 0; align-items: center; justify-content: center; transition: opacity var(--duration-fast) var(--ease-standard); }
10
+ .labelSlot[data-morphing] { clip-path: inset(-50% calc(var(--space-group) * -1)); }
11
+ .labelContent { display: inline-flex; flex: none; align-items: center; }
12
+ .labelPhase { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-related); white-space: nowrap; }
13
+ .labelPhase svg { flex: none; }
14
+ .primary { color: var(--primary-foreground); background: var(--primary); border-color: var(--primary); }
15
+ .secondary { color: var(--foreground); background: var(--surface); border-color: var(--border); }
16
+ .ghost { color: var(--text-secondary); background: transparent; border-color: transparent; }
17
+ .danger { color: var(--danger); background: var(--surface); border-color: var(--border); }
18
+ /* A disabled outlined button keeps its shape and fades only its label, so it still reads as a button that is off, not as plain text. */
19
+ .secondary:disabled, .danger:disabled { opacity: 1; color: var(--text-muted); background: var(--surface-muted); border-color: var(--border); }
20
+ .danger:disabled { color: color-mix(in oklab, var(--danger) 55%, var(--surface-muted)); }
21
+ .ghost:disabled { opacity: 1; color: var(--text-muted); }
22
+ @media (hover: hover) and (pointer: fine) {
23
+ .primary:hover:not(:disabled) { opacity: .91; box-shadow: var(--shadow-resting); }
24
+ .secondary:hover:not(:disabled) { background: var(--surface-muted); box-shadow: var(--shadow-resting); }
25
+ .ghost:hover:not(:disabled) { color: var(--foreground); background: var(--surface-muted); }
26
+ .danger:hover:not(:disabled) { border-color: var(--danger); background: var(--surface-muted); }
27
+ }
28
+ /* Color feedback on press, so touch and menu triggers (which never scale) still answer the finger. */
29
+ .primary:active:not(:disabled) { opacity: .84; }
30
+ .secondary:active:not(:disabled), .ghost:active:not(:disabled) { color: var(--foreground); background: var(--surface-muted); }
31
+ .danger:active:not(:disabled) { border-color: var(--danger); background: var(--surface-muted); }
32
+ .sm { min-height: var(--control-height-sm); padding-inline: var(--space-group); font-size: var(--text-sm); }
33
+ .md { min-height: var(--control-height-md); }
34
+ .lg { min-height: var(--control-height-lg); padding-inline: var(--space-comfort); font-size: var(--text-sm); }
35
+ .loader { position: absolute; left: 50%; top: 50%; display: grid; width: var(--space-inset); height: var(--space-inset); margin: calc(var(--space-inset) / -2); }
36
+ .spinner { border: 1.5px solid currentColor; border-right-color: transparent; border-radius: 50%; corner-shape: round; animation: spin .7s linear infinite; }
37
+ .loadingLabel { opacity: 0; }
38
+ @keyframes spin { to { transform: rotate(360deg); } }
39
+ @media (prefers-reduced-motion: reduce) { .button { transition: none; } .labelSlot { transition: opacity var(--duration-instant) linear; } .spinner { animation-duration: .01ms; animation-iteration-count: 1; } }
@@ -0,0 +1,59 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ export type ButtonGroupVariant = "outline" | "solid";
3
+ export type ButtonGroupSize = "sm" | "md";
4
+ export type ButtonGroupOrientation = "horizontal" | "vertical";
5
+ export interface ButtonGroupItem {
6
+ /** Stable key for the segment. */
7
+ id: string;
8
+ /** Visible text and the accessible name. Changing it crossfades in place, so "Share" can answer "Copied". */
9
+ label: string;
10
+ /** Every other label this segment can show, such as ["Copied"]. The segment sizes to the widest, so a label change never resizes it. */
11
+ reserve?: string[];
12
+ icon?: ReactNode;
13
+ /** Shows only the icon; the label becomes the accessible name and the tooltip. */
14
+ iconOnly?: boolean;
15
+ /** Visible content in place of the label, such as a live value. The label stays the accessible name. */
16
+ content?: ReactNode;
17
+ onSelect?: () => void;
18
+ /** Renders the segment as a link. */
19
+ href?: string;
20
+ disabled?: boolean;
21
+ }
22
+ export interface ButtonGroupMenuItem {
23
+ id: string;
24
+ label: string;
25
+ icon?: ReactNode;
26
+ onSelect?: () => void;
27
+ href?: string;
28
+ disabled?: boolean;
29
+ /** Colors the item as a destructive action. */
30
+ destructive?: boolean;
31
+ }
32
+ export interface ButtonGroupMenu {
33
+ /** Accessible name and tooltip of the chevron segment, such as "More actions". */
34
+ label: string;
35
+ items: ButtonGroupMenuItem[];
36
+ }
37
+ export interface ButtonGroupProps {
38
+ items: ButtonGroupItem[];
39
+ /** Adds a trailing chevron segment that opens these actions in a menu aligned to the group's edge. */
40
+ menu?: ButtonGroupMenu;
41
+ /** Accessible name of the group, such as "Document actions". */
42
+ label: string;
43
+ variant?: ButtonGroupVariant;
44
+ size?: ButtonGroupSize;
45
+ orientation?: ButtonGroupOrientation;
46
+ /** When the row does not fit its container, segments with an icon drop their label and keep the icon. Horizontal only. */
47
+ collapseLabels?: boolean;
48
+ /** Disables every segment and the menu. */
49
+ disabled?: boolean;
50
+ className?: string;
51
+ style?: CSSProperties;
52
+ }
53
+ /**
54
+ * Related actions joined into one surface: a shared border and radius, hairline dividers, no gaps. One soft highlight glides
55
+ * between segments under the pointer or keyboard focus, the pressed segment answers in place, and an optional chevron
56
+ * segment opens more actions in a menu aligned to the group's edge.
57
+ */
58
+ export declare function ButtonGroup({ items, menu, label, variant, size, orientation, collapseLabels, disabled, className, style }: ButtonGroupProps): import("react").JSX.Element;
59
+ export default ButtonGroup;
@@ -0,0 +1,280 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { isValidElement, useEffect, useLayoutEffect, useRef, useState } from "react";
4
+ import * as DropdownPrimitive from "@radix-ui/react-dropdown-menu";
5
+ import { AnimatePresence, animate, motion, useMotionValue, useReducedMotion } from "motion/react";
6
+ import { ChevronDown } from "lucide-react";
7
+ import { motionTokens } from "../../lib/motion-tokens.js";
8
+ import styles from "./button-group.module.css";
9
+ /** Key of the chevron segment. Plain text, because the HTML parser rewrites control characters in server-rendered attributes. */
10
+ const MENU = "button-group-menu";
11
+ const rest = { opacity: 1, y: 0, scale: 1, filter: "blur(0px)" };
12
+ const textIn = { opacity: 0, y: 4, filter: `blur(${motionTokens.blur.soft}px)` };
13
+ const textOut = { opacity: 0, y: -3, filter: `blur(${motionTokens.blur.soft}px)`, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } };
14
+ const fadeIn = { ...rest, opacity: 0 };
15
+ const fadeOut = { opacity: 0, transition: { duration: motionTokens.duration.instant } };
16
+ /** Names the icon element, so swapping Link for Check morphs while a re-render of the same icon stays still. */
17
+ function iconKey(node) {
18
+ if (!isValidElement(node))
19
+ return node == null || typeof node === "boolean" ? "" : String(node);
20
+ const type = node.type;
21
+ return typeof type === "string" ? type : type?.displayName ?? type?.name ?? "icon";
22
+ }
23
+ /** Springs the slot to the natural width of its content when the label changes; other resizes (a late web font, collapsing labels) jump. */
24
+ function useMorphWidth(content, key, reduced) {
25
+ const width = useMotionValue("auto");
26
+ const lastKey = useRef(key), armedUntil = useRef(0);
27
+ useLayoutEffect(() => {
28
+ if (lastKey.current === key)
29
+ return;
30
+ lastKey.current = key;
31
+ armedUntil.current = performance.now() + 700;
32
+ }, [key]);
33
+ useEffect(() => {
34
+ const node = content.current, slot = node?.parentElement;
35
+ if (!node || !slot || typeof ResizeObserver === "undefined")
36
+ return;
37
+ let measured = false;
38
+ const observer = new ResizeObserver(([entry]) => {
39
+ if (!entry)
40
+ return;
41
+ const next = entry.contentRect.width;
42
+ if (!next || !measured || reduced || performance.now() > armedUntil.current) {
43
+ measured = next > 0;
44
+ width.jump(next || "auto");
45
+ delete slot.dataset.morphing;
46
+ return;
47
+ }
48
+ slot.dataset.morphing = "";
49
+ animate(width, next, { ...motionTokens.spring.morph, onComplete: () => { delete slot.dataset.morphing; } });
50
+ });
51
+ observer.observe(node);
52
+ return () => observer.disconnect();
53
+ }, [content, reduced, width]);
54
+ return width;
55
+ }
56
+ /**
57
+ * Icon and label of one segment. Every reserved label sits invisibly in the same grid cell, so the segment is as wide as its widest
58
+ * state and a new label crossfades inside a fixed box: it rises in from a soft blur while the old one lifts away. A label nobody
59
+ * reserved still never snaps: the slot springs to its width.
60
+ */
61
+ function SegmentContent({ item, reduced }) {
62
+ const contentRef = useRef(null);
63
+ const key = `${iconKey(item.icon)}|${item.content !== undefined ? "\u0000content" : item.iconOnly ? "" : item.label}`;
64
+ const width = useMorphWidth(contentRef, key, reduced);
65
+ const sizers = item.iconOnly || item.content !== undefined ? [] : [...new Set([item.label, ...(item.reserve ?? [])])];
66
+ return _jsx(motion.span, { className: styles.slot, style: { width }, "aria-hidden": "true", children: _jsxs("span", { ref: contentRef, className: styles.slotContent, children: [sizers.map(text => _jsxs("span", { className: `${styles.phase} ${styles.sizer}`, children: [item.icon ? _jsx("span", { className: styles.iconBox }) : null, _jsx("span", { className: styles.label, "data-collapsible": item.icon ? "" : undefined, children: text })] }, text)), _jsx(AnimatePresence, { initial: false, children: _jsxs(motion.span, { className: styles.phase, initial: reduced ? fadeIn : textIn, animate: rest, exit: reduced ? fadeOut : textOut, transition: reduced ? { duration: motionTokens.duration.instant } : { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] }, children: [item.icon ? _jsx("span", { className: styles.icon, children: item.icon }) : null, item.content !== undefined ? _jsx("span", { className: styles.value, children: item.content }) : item.iconOnly ? null : _jsx("span", { className: styles.label, "data-collapsible": item.icon ? "" : undefined, children: item.label })] }, key) })] }) });
67
+ }
68
+ const inside = (node, x, y) => {
69
+ const rect = node.getBoundingClientRect();
70
+ return x >= rect.left && x < rect.right && y >= rect.top && y < rect.bottom;
71
+ };
72
+ /** Segments are found by their data-key, so the group needs no ref per segment. */
73
+ const segmentsIn = (root) => Array.from(root?.querySelectorAll(":scope > [data-key]") ?? []);
74
+ const segmentIn = (root, key) => key === null ? undefined : segmentsIn(root).find(node => node.dataset.key === key);
75
+ /** The segment's box inside the group at sub-pixel precision, undoing any scale an ancestor applies. */
76
+ function boxOf(group, node) {
77
+ const outer = group.getBoundingClientRect(), inner = node.getBoundingClientRect();
78
+ // offsetWidth is rounded to whole pixels, so only a real transform (more than a pixel apart) counts as scale.
79
+ const scale = group.offsetWidth && Math.abs(outer.width - group.offsetWidth) > 1 ? outer.width / group.offsetWidth : 1;
80
+ return [(inner.left - outer.left) / scale - group.clientLeft + group.scrollLeft, (inner.top - outer.top) / scale - group.clientTop + group.scrollTop, inner.width / scale, inner.height / scale];
81
+ }
82
+ const inert = (node) => node.disabled || node.getAttribute("aria-disabled") === "true";
83
+ /**
84
+ * Related actions joined into one surface: a shared border and radius, hairline dividers, no gaps. One soft highlight glides
85
+ * between segments under the pointer or keyboard focus, the pressed segment answers in place, and an optional chevron
86
+ * segment opens more actions in a menu aligned to the group's edge.
87
+ */
88
+ export function ButtonGroup({ items, menu, label, variant = "outline", size = "md", orientation = "horizontal", collapseLabels = true, disabled = false, className, style }) {
89
+ const reduced = useReducedMotion() ?? false;
90
+ const root = useRef(null);
91
+ const vertical = orientation === "vertical";
92
+ // The highlight sits on the pointer's segment, else the pressed one (touch), else the open menu's chevron, else keyboard focus.
93
+ const [hover, setHover] = useState(null);
94
+ const [pressed, setPressed] = useState(null);
95
+ const [focused, setFocused] = useState(null);
96
+ const [menuOpen, setMenuOpen] = useState(false);
97
+ const active = hover ?? pressed ?? (menuOpen ? MENU : null) ?? focused;
98
+ const keys = [...items.map(item => item.id), ...(menu ? [MENU] : [])];
99
+ const activeIndex = active ? keys.indexOf(active) : -1;
100
+ const inertKey = `${disabled}${items.map(item => item.disabled ? 1 : 0).join("")}`;
101
+ const x = useMotionValue(0), y = useMotionValue(0), width = useMotionValue(0), height = useMotionValue(0), opacity = useMotionValue(0);
102
+ const shown = useRef(null);
103
+ useLayoutEffect(() => {
104
+ const node = segmentIn(root.current, active);
105
+ // Hovering a disabled segment shows nothing; focus on one still shows where the keyboard is.
106
+ if (!node || (inert(node) && active !== focused)) {
107
+ if (shown.current)
108
+ animate(opacity, 0, { duration: reduced ? motionTokens.duration.instant : motionTokens.duration.fast, ease: [...motionTokens.ease.standard] });
109
+ shown.current = null;
110
+ return;
111
+ }
112
+ const target = boxOf(root.current, node);
113
+ const values = [x, y, width, height];
114
+ if (!shown.current || reduced) {
115
+ // Arriving from nowhere, it appears in place; only moves between segments travel.
116
+ values.forEach((value, index) => value.jump(target[index]));
117
+ animate(opacity, 1, { duration: reduced ? motionTokens.duration.instant : motionTokens.duration.fast, ease: [...motionTokens.ease.standard] });
118
+ }
119
+ else {
120
+ values.forEach((value, index) => animate(value, target[index], motionTokens.spring.snappy));
121
+ animate(opacity, 1, { duration: motionTokens.duration.fast });
122
+ }
123
+ shown.current = active;
124
+ // inertKey re-checks a segment that turns disabled under a still pointer.
125
+ // eslint-disable-next-line react-hooks/exhaustive-deps
126
+ }, [active, inertKey, reduced, x, y, width, height, opacity]);
127
+ // Segments resize when a label morphs or labels collapse; the highlight stays locked to its segment.
128
+ const keyList = keys.join("\u0000");
129
+ useEffect(() => {
130
+ const group = root.current;
131
+ if (!group || typeof ResizeObserver === "undefined")
132
+ return;
133
+ const observer = new ResizeObserver(() => {
134
+ const node = segmentIn(group, shown.current);
135
+ if (!node)
136
+ return;
137
+ const target = boxOf(group, node);
138
+ [x, y, width, height].forEach((value, index) => {
139
+ if (Math.abs(value.get() - target[index]) < .01)
140
+ return;
141
+ if (value.isAnimating() && !reduced)
142
+ animate(value, target[index], motionTokens.spring.snappy);
143
+ else
144
+ value.jump(target[index]);
145
+ });
146
+ });
147
+ observer.observe(group);
148
+ segmentsIn(group).forEach(node => observer.observe(node));
149
+ return () => observer.disconnect();
150
+ }, [keyList, reduced, x, y, width, height]);
151
+ // Labels collapse to icons when the row overflows, and come back once the container is wide enough for the full row again.
152
+ const [overflowing, setCompact] = useState(false);
153
+ const fullWidth = useRef(0), blockedAt = useRef(0);
154
+ const collapsible = collapseLabels && !vertical && items.some(item => item.icon && !item.iconOnly && item.content === undefined);
155
+ const compact = collapsible && overflowing;
156
+ useEffect(() => {
157
+ const group = root.current, parent = group?.parentElement;
158
+ if (!group || !parent || !collapsible || typeof ResizeObserver === "undefined")
159
+ return;
160
+ const check = () => {
161
+ const box = getComputedStyle(parent);
162
+ const available = parent.clientWidth - parseFloat(box.paddingLeft) - parseFloat(box.paddingRight);
163
+ if (group.dataset.compact === undefined) {
164
+ if (group.scrollWidth > group.clientWidth + 1) {
165
+ fullWidth.current = group.scrollWidth + 2;
166
+ blockedAt.current = available;
167
+ setCompact(true);
168
+ }
169
+ }
170
+ else if (available > blockedAt.current && available >= fullWidth.current)
171
+ setCompact(false);
172
+ };
173
+ const observer = new ResizeObserver(check);
174
+ observer.observe(group);
175
+ observer.observe(parent);
176
+ return () => observer.disconnect();
177
+ }, [collapsible, keyList]);
178
+ // Pressing tracks the segment under the finger, so touch gets the same highlight a pointer gets on hover.
179
+ useEffect(() => {
180
+ if (!pressed)
181
+ return;
182
+ const release = () => setPressed(null);
183
+ window.addEventListener("pointerup", release);
184
+ window.addEventListener("pointercancel", release);
185
+ return () => { window.removeEventListener("pointerup", release); window.removeEventListener("pointercancel", release); };
186
+ }, [pressed]);
187
+ const hit = (event) => {
188
+ for (const node of segmentsIn(root.current))
189
+ if (inside(node, event.clientX, event.clientY))
190
+ return inert(node) ? null : node.dataset.key ?? null;
191
+ return null;
192
+ };
193
+ const onPointerMove = (event) => { if (event.pointerType !== "touch")
194
+ setHover(hit(event)); };
195
+ const onPointerLeave = () => setHover(null);
196
+ const onPointerDown = (event) => { if (event.button === 0)
197
+ setPressed(hit(event)); };
198
+ // Keyboard focus shows the highlight; a mouse click focuses without it, so nothing stays lit after the pointer leaves.
199
+ const onFocus = (event) => {
200
+ const target = event.target;
201
+ const key = target.dataset.key;
202
+ if (key === undefined)
203
+ return;
204
+ let visible = true;
205
+ try {
206
+ visible = target.matches(":focus-visible");
207
+ }
208
+ catch { /* older engines */ }
209
+ setFocused(visible ? key : null);
210
+ };
211
+ const onBlur = (event) => { if (!root.current?.contains(event.relatedTarget))
212
+ setFocused(null); };
213
+ // Tab visits every segment in order; arrow keys along the orientation, Home and End also move between them.
214
+ const onKeyDown = (event) => {
215
+ if (event.defaultPrevented)
216
+ return;
217
+ // Disabled segments stay focusable (aria-disabled), so the keyboard can find them and hear why nothing happens.
218
+ const list = segmentsIn(root.current).filter(node => !node.disabled);
219
+ const index = list.findIndex(node => node === document.activeElement);
220
+ if (index < 0)
221
+ return;
222
+ const rtl = !vertical && getComputedStyle(root.current).direction === "rtl";
223
+ const forward = vertical ? "ArrowDown" : rtl ? "ArrowLeft" : "ArrowRight";
224
+ const backward = vertical ? "ArrowUp" : rtl ? "ArrowRight" : "ArrowLeft";
225
+ const last = list.length - 1;
226
+ const target = event.key === forward ? (index === last ? 0 : index + 1) : event.key === backward ? (index === 0 ? last : index - 1) : event.key === "Home" ? 0 : event.key === "End" ? last : -1;
227
+ if (target < 0)
228
+ return;
229
+ event.preventDefault();
230
+ list[target].focus();
231
+ };
232
+ // A label that changes right after its segment is pressed, such as "Copied", is announced once; the quiet revert is not.
233
+ const [announcement, setAnnouncement] = useState("");
234
+ const labels = useRef(null);
235
+ const activated = useRef({ key: "", at: 0 });
236
+ const labelList = items.map(item => `${item.id}\u0000${item.label}`).join("\u0001");
237
+ useEffect(() => {
238
+ const previous = labels.current;
239
+ labels.current = new Map(items.map(item => [item.id, item.label]));
240
+ if (!previous)
241
+ return;
242
+ const recent = performance.now() - activated.current.at < 1000;
243
+ const changed = items.filter(item => recent && item.id === activated.current.key && previous.has(item.id) && previous.get(item.id) !== item.label && item.content === undefined);
244
+ if (changed.length)
245
+ setAnnouncement(changed.map(item => item.label).join(", "));
246
+ // labelList carries the only part of items this effect reads.
247
+ // eslint-disable-next-line react-hooks/exhaustive-deps
248
+ }, [labelList]);
249
+ const select = (item) => { activated.current = { key: item.id, at: performance.now() }; item.onSelect?.(); };
250
+ const quiet = (index) => activeIndex >= 0 && (index === activeIndex || index === activeIndex + 1) ? "" : undefined;
251
+ const segments = items.map((item, index) => {
252
+ const off = item.disabled && !disabled;
253
+ const named = item.iconOnly || item.content !== undefined || (compact && !!item.icon);
254
+ const common = {
255
+ className: [styles.segment, item.iconOnly ? styles.iconOnly : ""].filter(Boolean).join(" "),
256
+ "data-key": item.id,
257
+ "data-quiet": quiet(index),
258
+ "data-collapsible": item.icon && !item.iconOnly && item.content === undefined ? "" : undefined,
259
+ "aria-label": named ? item.label : undefined,
260
+ title: item.iconOnly || (compact && item.icon) ? item.label : undefined,
261
+ };
262
+ const body = _jsx("span", { className: styles.content, children: _jsx(SegmentContent, { item: item, reduced: reduced }) });
263
+ if (item.href && !off && !disabled)
264
+ return _jsxs("a", { ...common, href: item.href, onClick: () => select(item), children: [body, named ? null : _jsx("span", { className: styles.srOnly, children: item.label })] }, item.id);
265
+ return _jsxs("button", { ...common, type: "button", disabled: disabled, "aria-disabled": off || undefined, onClick: off ? undefined : () => select(item), children: [body, named ? null : _jsx("span", { className: styles.srOnly, children: item.label })] }, item.id);
266
+ });
267
+ const classes = [styles.group, styles[variant], styles[size], vertical ? styles.vertical : "", compact ? styles.compact : "", className].filter(Boolean).join(" ");
268
+ const group = _jsxs("div", { ref: root, className: classes, style: style, role: "group", "aria-label": label, "aria-disabled": disabled || undefined, "data-orientation": orientation, "data-compact": compact ? "" : undefined, onPointerMove: onPointerMove, onPointerLeave: onPointerLeave, onPointerDown: onPointerDown, onFocus: onFocus, onBlur: onBlur, onKeyDown: onKeyDown, children: [_jsx(motion.span, { className: styles.highlight, style: { x, y, width, height, opacity }, "data-pressed": pressed && pressed === active ? "" : undefined, "aria-hidden": "true" }), segments, menu ? _jsx(DropdownPrimitive.Trigger, { className: [styles.segment, styles.trigger].join(" "), "data-key": MENU, "data-quiet": quiet(items.length), "aria-label": menu.label, title: menu.label, disabled: disabled, children: _jsx("span", { className: styles.content, children: _jsx(ChevronDown, { className: styles.chevron, "aria-hidden": "true" }) }) }) : null, _jsx("span", { className: styles.srOnly, "aria-live": "polite", children: announcement })] });
269
+ if (!menu)
270
+ return group;
271
+ // Non-modal: no scroll lock, so opening the menu never shifts the page by a scrollbar's width.
272
+ return _jsxs(DropdownPrimitive.Root, { open: menuOpen, onOpenChange: setMenuOpen, modal: false, children: [group, _jsx(DropdownPrimitive.Portal, { children: _jsx(DropdownPrimitive.Content, { className: styles.menu, side: "bottom", align: vertical ? "start" : "end", alignOffset: -1, sideOffset: 6, collisionPadding: 12, loop: true, children: menu.items.map((action, index) => {
273
+ const classes = [styles.item, action.destructive ? styles.destructive : ""].filter(Boolean).join(" ");
274
+ const body = _jsxs(_Fragment, { children: [action.icon ? _jsx("span", { className: styles.itemIcon, "aria-hidden": "true", children: action.icon }) : null, _jsx("span", { className: styles.itemLabel, children: action.label })] });
275
+ return action.href
276
+ ? _jsx(DropdownPrimitive.Item, { className: classes, style: { "--i": index }, disabled: action.disabled, onSelect: action.onSelect, asChild: true, children: _jsx("a", { href: action.href, children: body }) }, action.id)
277
+ : _jsx(DropdownPrimitive.Item, { className: classes, style: { "--i": index }, disabled: action.disabled, onSelect: action.onSelect, children: body }, action.id);
278
+ }) }) })] });
279
+ }
280
+ export default ButtonGroup;
@@ -0,0 +1,87 @@
1
+ /* One surface, several actions: a shared border and radius, hairline dividers, no gaps. A single soft highlight travels under the
2
+ segments instead of each one lighting up; the dividers beside it step aside so it reads as one shape. */
3
+ .group { --bg-h: var(--control-height-md); --bg-pad-x: 14px; --bg-inset: 3px; --bg-radius: var(--radius-control); --bg-divider: var(--border); --bg-divider-inset: 11px; --bg-highlight: var(--surface-muted); --bg-highlight-pressed: color-mix(in oklab, var(--surface-muted), var(--border-strong) 30%); --bg-muted-ink: var(--text-secondary);
4
+ position: relative; isolation: isolate; display: inline-flex; max-width: 100%; align-items: stretch; overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: none; border: 1px solid var(--border); border-radius: var(--bg-radius); background: var(--surface); color: var(--foreground); vertical-align: middle; }
5
+ .group::-webkit-scrollbar { display: none; }
6
+ .sm { --bg-h: var(--control-height-sm); --bg-pad-x: 12px; --bg-divider-inset: 9px; }
7
+ .md { --bg-h: var(--control-height-md); }
8
+ /* Solid is the primary group: the foreground fill of a primary button, with the highlight and dividers mixed from the page color. */
9
+ .solid { --bg-divider: color-mix(in oklab, var(--background) 20%, var(--foreground)); --bg-highlight: color-mix(in oklab, var(--background) 13%, var(--foreground)); --bg-highlight-pressed: color-mix(in oklab, var(--background) 22%, var(--foreground)); --bg-muted-ink: color-mix(in oklab, var(--background) 72%, var(--foreground)); border-color: var(--foreground); background: var(--foreground); color: var(--background); }
10
+ .vertical { flex-direction: column; overflow: visible; --bg-divider-inset: 10px; }
11
+ .group[aria-disabled="true"] { opacity: .52; }
12
+
13
+ /* The travelling highlight. Motion sets its box to the active segment; the visible shape sits inset so the border stays crisp. */
14
+ .highlight { position: absolute; z-index: 0; top: 0; left: 0; pointer-events: none; }
15
+ .highlight::before { position: absolute; inset: var(--bg-inset); border-radius: calc(var(--bg-radius) - var(--bg-inset) - 1px); corner-shape: var(--corner-shape); background: var(--bg-highlight); content: ""; transition: background-color var(--duration-fast) var(--ease-standard); }
16
+ .highlight[data-pressed]::before { background: var(--bg-highlight-pressed); transition-duration: var(--duration-instant); }
17
+
18
+ .segment { position: relative; z-index: 1; display: inline-flex; flex: none; min-width: calc(var(--bg-h) - 2px); height: calc(var(--bg-h) - 2px); align-items: center; justify-content: center; margin: 0; padding: 0 var(--bg-pad-x); border: 0; border-radius: 0; corner-shape: round; background: transparent; color: inherit; font: inherit; font-size: var(--text-sm); font-weight: 500; letter-spacing: var(--tracking-body); line-height: 1; white-space: nowrap; text-decoration: none; cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent; }
19
+ .vertical .segment { width: 100%; justify-content: flex-start; }
20
+ .vertical .iconOnly, .vertical .trigger { justify-content: center; padding: 0; }
21
+ .iconOnly { width: calc(var(--bg-h) - 2px); padding: 0; }
22
+ .trigger { width: calc(var(--bg-h) - 6px); min-width: 0; padding: 0; color: var(--bg-muted-ink); transition: color var(--duration-fast) var(--ease-standard); }
23
+ .trigger[data-state="open"] { color: inherit; }
24
+ .segment[aria-disabled="true"] { cursor: not-allowed; }
25
+ .segment[aria-disabled="true"] .content { opacity: .4; }
26
+ .group[aria-disabled="true"] .segment { cursor: not-allowed; }
27
+ /* Collapsed rows keep the icon and drop the label; the label moves to aria-label and the tooltip. */
28
+ .compact .segment[data-collapsible] { width: calc(var(--bg-h) - 2px); padding: 0; }
29
+ .compact .label[data-collapsible] { display: none; }
30
+
31
+ /* Hairline dividers sit between segments and fade where the highlight arrives, so it never sits beside a line. */
32
+ .segment + .segment::before { position: absolute; top: var(--bg-divider-inset); bottom: var(--bg-divider-inset); left: -.5px; width: 1px; background: var(--bg-divider); content: ""; pointer-events: none; transition: opacity var(--duration-fast) var(--ease-standard); }
33
+ .vertical .segment + .segment::before { top: -.5px; right: var(--bg-divider-inset); bottom: auto; left: var(--bg-divider-inset); width: auto; height: 1px; }
34
+ .segment[data-quiet]::before { opacity: 0; }
35
+
36
+ /* The press answers inside the segment: content dips and springs back, the group and its neighbours stay still. */
37
+ .content { position: relative; display: inline-flex; align-items: center; transition: transform var(--duration-spring) var(--ease-spring), opacity var(--duration-fast) var(--ease-standard); }
38
+ .segment:active:not([aria-disabled="true"]) .content { transform: scale(.97); transition-duration: var(--duration-instant); transition-timing-function: var(--ease-standard); }
39
+ .iconOnly:active:not([aria-disabled="true"]) .content { transform: scale(.9); }
40
+ /* The chevron anchors the menu, so it answers with the deeper highlight only. */
41
+ .trigger:active .content { transform: none; }
42
+ .chevron { width: 16px; height: 16px; stroke-width: 1.75; transition: transform var(--duration-spring) var(--ease-spring); }
43
+ .trigger[data-state="open"] .chevron { transform: rotate(180deg); }
44
+
45
+ /* Reserved labels and the visible one share one grid cell, so the widest state sets the width and a change crossfades in place.
46
+ A label nobody reserved springs the slot to its width instead; content is clipped only while that happens. */
47
+ .slot { position: relative; display: inline-flex; min-width: 0; align-items: center; justify-content: center; }
48
+ .slot[data-morphing] { clip-path: inset(-50% calc(var(--space-group) * -1)); }
49
+ .slotContent { display: inline-grid; flex: none; align-items: center; justify-items: center; }
50
+ .slotContent > * { grid-area: 1 / 1; }
51
+ .phase { display: inline-flex; align-items: center; gap: var(--space-related); white-space: nowrap; }
52
+ .sizer { visibility: hidden; pointer-events: none; }
53
+ .iconBox { display: inline-block; width: 16px; height: 16px; flex: none; }
54
+ .sm .phase { gap: 6px; }
55
+ .icon { display: inline-flex; flex: none; }
56
+ .icon svg { width: 16px; height: 16px; stroke-width: 1.75; }
57
+ .value { display: inline-flex; align-items: center; font-variant-numeric: tabular-nums; }
58
+ .label { display: inline-block; }
59
+ .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
60
+
61
+ /* The menu grows from the group's edge: offset toward it, scale from the Radix origin, and leave faster than it arrives.
62
+ Transitions instead of keyframes, so a close that interrupts the open reverses from where the menu is. */
63
+ .menu { --menu-x: 0px; --menu-y: -5px; z-index: 60; min-width: min(13rem, var(--radix-dropdown-menu-content-available-width)); max-width: var(--radix-dropdown-menu-content-available-width); padding: 5px; border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--surface-raised); color: var(--foreground); box-shadow: var(--shadow-floating); transform-origin: var(--radix-dropdown-menu-content-transform-origin); transition: opacity var(--duration-fast) var(--ease-enter), transform var(--duration-spring) var(--ease-spring); }
64
+ .menu[data-side="top"] { --menu-y: 5px; }
65
+ .menu[data-side="left"] { --menu-x: 5px; --menu-y: 0px; }
66
+ .menu[data-side="right"] { --menu-x: -5px; --menu-y: 0px; }
67
+ @starting-style {
68
+ .menu[data-state="open"] { opacity: 0; transform: translate(var(--menu-x), var(--menu-y)) scale(.97); }
69
+ .menu[data-state="open"] .item { opacity: 0; transform: translate(calc(var(--menu-x) * .4), calc(var(--menu-y) * .4)); }
70
+ }
71
+ /* Radix unmounts when the exit animation ends, so a no-op keyframe times the exit while the transition does the visual work. */
72
+ .menu[data-state="closed"] { opacity: 0; transform: translate(calc(var(--menu-x) * .5), calc(var(--menu-y) * .5)) scale(.985); transition: opacity 130ms var(--ease-standard), transform 130ms var(--ease-standard); animation: menu-exit 130ms linear both; pointer-events: none; }
73
+ .item { display: flex; min-height: 36px; align-items: center; gap: 10px; padding: 0 11px; border-radius: calc(var(--radius-container) - 6px); corner-shape: var(--corner-shape); color: inherit; font-size: var(--text-sm); text-decoration: none; cursor: pointer; outline: none; transition: opacity var(--duration-standard) var(--ease-enter) calc(min(var(--i, 0), 4) * 35ms), transform var(--duration-standard) var(--ease-enter) calc(min(var(--i, 0), 4) * 35ms); }
74
+ .item[data-highlighted] { background: var(--surface-muted); }
75
+ .item[data-disabled] { cursor: default; opacity: .45; }
76
+ .itemIcon { display: inline-flex; flex: none; color: var(--text-secondary); }
77
+ .itemIcon svg { width: 16px; height: 16px; stroke-width: 1.75; }
78
+ .itemLabel { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
79
+ .destructive, .destructive .itemIcon { color: var(--danger); }
80
+ .destructive[data-highlighted] { background: color-mix(in oklab, var(--danger) 8%, var(--surface)); }
81
+ @keyframes menu-exit { to { --menu-exit: 1; } }
82
+
83
+ @media (prefers-reduced-motion: reduce) {
84
+ .content, .chevron, .segment::before, .highlight::before, .item { transition: none; }
85
+ .segment:active .content, .iconOnly:active .content { transform: none; }
86
+ .menu, .menu[data-state="closed"] { transform: none; transition: opacity var(--duration-instant) linear; }
87
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * A link that looks exactly like a Button. Navigation is a link, never a button that pushes a route, so it opens in a new
3
+ * tab, shows its address and works without JavaScript. No client code: it renders on the server as a plain <a>.
4
+ *
5
+ * <ButtonLink href="/sign-in">Sign in</ButtonLink>
6
+ * <ButtonLink href="/docs" variant="secondary" as={Link}>Read the docs</ButtonLink>
7
+ *
8
+ * Pass `as` to render your router's link (next/link, a wrapped React Router Link); it receives `href`, `className`,
9
+ * `data-variant` and the other anchor props.
10
+ */
11
+ import type { AnchorHTMLAttributes, ComponentType, ReactNode } from "react";
12
+ import type { ButtonSize, ButtonVariant } from "../button/button.js";
13
+ export interface ButtonLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
14
+ href: string;
15
+ variant?: ButtonVariant;
16
+ size?: ButtonSize;
17
+ /** The element to render instead of <a>, such as next/link. */
18
+ as?: ComponentType<AnchorHTMLAttributes<HTMLAnchorElement> & {
19
+ href: string;
20
+ }>;
21
+ children: ReactNode;
22
+ }
23
+ export declare function ButtonLink({ href, variant, size, as: Anchor, className, children, ...props }: ButtonLinkProps): import("react").JSX.Element;
24
+ export default ButtonLink;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import buttonStyles from "../button/button.module.css";
3
+ import styles from "./button-link.module.css";
4
+ export function ButtonLink({ href, variant = "primary", size = "md", as: Anchor, className, children, ...props }) {
5
+ const classes = [buttonStyles.button, buttonStyles[variant], buttonStyles[size], styles.link, className].filter(Boolean).join(" ");
6
+ const content = _jsx("span", { className: styles.content, children: children });
7
+ if (Anchor)
8
+ return _jsx(Anchor, { ...props, href: href, "data-variant": variant, className: classes, children: content });
9
+ return _jsx("a", { ...props, href: href, "data-variant": variant, className: classes, children: content });
10
+ }
11
+ export default ButtonLink;
@@ -0,0 +1,7 @@
1
+ /* Button's look on a link: no underline in any state, and a CSS press in place of Button's Motion press. */
2
+ .link { text-decoration: none; transition-property: color, background, border-color, opacity, box-shadow, transform; }
3
+ .link:hover { text-decoration: none; }
4
+ .link:active { transform: scale(.97); transition-duration: var(--duration-instant); }
5
+ .content { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-related); white-space: nowrap; }
6
+ .content svg { flex: none; }
7
+ @media (prefers-reduced-motion: reduce) { .link:active { transform: none; } }
@@ -0,0 +1,22 @@
1
+ export type CalendarDateMatcher = (date: Date) => boolean;
2
+ export interface CalendarProps {
3
+ value?: Date;
4
+ onChange?: (date: Date) => void;
5
+ month?: Date;
6
+ onMonthChange?: (month: Date) => void;
7
+ minDate?: Date;
8
+ maxDate?: Date;
9
+ disabledDates?: CalendarDateMatcher;
10
+ locale?: string;
11
+ className?: string;
12
+ /** Adds a Today button that slides back to the current month and selects today when it is available. */
13
+ showToday?: boolean;
14
+ }
15
+ declare const startOfDay: (date: Date) => Date;
16
+ declare const sameDay: (a?: Date, b?: Date) => boolean;
17
+ declare const addDays: (date: Date, amount: number) => Date;
18
+ declare const addMonths: (date: Date, amount: number) => Date;
19
+ /** The viewer's local date (lib/use-today.ts, shared with date-range-picker). Re-exported so existing imports from calendar keep working. */
20
+ export { useToday } from "../../lib/use-today.js";
21
+ export declare function Calendar({ value, onChange, month: controlledMonth, onMonthChange, minDate, maxDate, disabledDates, locale, className, showToday, }: CalendarProps): import("react").JSX.Element;
22
+ export { addDays, addMonths, sameDay, startOfDay };