@justanarthur/modo-ui 0.5.0 → 0.6.0-canary.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 (461) hide show
  1. package/DESIGN.md +608 -0
  2. package/README.md +61 -39
  3. package/blocks/sidebar-app/examples.mdx +33 -0
  4. package/blocks/sidebar-app/index.tsx +218 -0
  5. package/blocks/sidebar-app/inset-topbar.tsx +25 -0
  6. package/blocks/sidebar-app/menu-grid.ts +26 -0
  7. package/blocks/sidebar-app/nav-data.ts +36 -0
  8. package/blocks/sidebar-app/search-field.tsx +48 -0
  9. package/blocks/sidebar-app/user-footer.tsx +67 -0
  10. package/blocks/sidebar-app/workspace-header.tsx +129 -0
  11. package/components/AI/input-message/examples.mdx +124 -0
  12. package/components/AI/input-message/file-thumbnail.tsx +173 -0
  13. package/components/AI/input-message/index.tsx +1383 -0
  14. package/components/AI/thinking-indicator/examples.mdx +27 -0
  15. package/components/AI/thinking-indicator/index.tsx +210 -0
  16. package/components/accordion/examples.mdx +148 -0
  17. package/components/accordion/index.tsx +1034 -0
  18. package/components/avatar/examples.mdx +95 -0
  19. package/components/avatar/index.tsx +129 -0
  20. package/components/badge/change-demo.tsx +29 -0
  21. package/components/badge/examples.mdx +120 -0
  22. package/components/badge/index.tsx +225 -0
  23. package/components/badge/variants.ts +73 -0
  24. package/components/button/examples.mdx +164 -0
  25. package/components/button/index.tsx +416 -0
  26. package/components/button/loading-demo.tsx +25 -0
  27. package/components/button/variants.ts +166 -0
  28. package/components/card/examples.mdx +212 -0
  29. package/components/card/index.tsx +1037 -0
  30. package/components/copy/examples.mdx +87 -0
  31. package/components/copy/index.tsx +482 -0
  32. package/components/inputs/checkbox/examples.mdx +83 -0
  33. package/components/inputs/checkbox/index.tsx +611 -0
  34. package/components/inputs/checkbox/select-all-demo.tsx +48 -0
  35. package/components/inputs/color-picker/examples.mdx +72 -0
  36. package/components/inputs/color-picker/index.tsx +1955 -0
  37. package/components/inputs/color-picker/variants.ts +401 -0
  38. package/components/inputs/combobox/examples.mdx +273 -0
  39. package/components/inputs/combobox/index.tsx +1436 -0
  40. package/components/inputs/combobox/remote-demo.tsx +101 -0
  41. package/components/inputs/date-time/calendar/examples.mdx +34 -0
  42. package/components/inputs/date-time/calendar/index.tsx +351 -0
  43. package/components/inputs/date-time/calendar/use-month-window.ts +163 -0
  44. package/components/inputs/date-time/date-picker/examples.mdx +166 -0
  45. package/components/inputs/date-time/date-picker/index.tsx +385 -0
  46. package/components/inputs/date-time/date-picker/masked-date-input.tsx +336 -0
  47. package/components/inputs/date-time/date-range-picker/examples.mdx +29 -0
  48. package/components/inputs/date-time/date-range-picker/index.tsx +217 -0
  49. package/components/inputs/date-time/duration-picker/examples.mdx +16 -0
  50. package/components/inputs/date-time/duration-picker/index.tsx +189 -0
  51. package/components/inputs/date-time/month-picker/examples.mdx +16 -0
  52. package/components/inputs/date-time/month-picker/index.tsx +190 -0
  53. package/components/inputs/date-time/picker-shell.tsx +395 -0
  54. package/components/inputs/date-time/time-columns.tsx +91 -0
  55. package/components/inputs/date-time/time-picker/examples.mdx +26 -0
  56. package/components/inputs/date-time/time-picker/index.tsx +166 -0
  57. package/components/inputs/field/examples.mdx +113 -0
  58. package/components/inputs/field/index.tsx +267 -0
  59. package/components/inputs/file-dropzone/examples.mdx +73 -0
  60. package/components/inputs/file-dropzone/file-row.tsx +201 -0
  61. package/components/inputs/file-dropzone/index.tsx +296 -0
  62. package/components/inputs/file-dropzone/upload-demo.tsx +67 -0
  63. package/components/inputs/file-dropzone/variants.ts +94 -0
  64. package/components/inputs/input/examples.mdx +135 -0
  65. package/components/inputs/input/index.tsx +494 -0
  66. package/components/inputs/input/password-demo.tsx +30 -0
  67. package/components/inputs/label/examples.mdx +67 -0
  68. package/components/inputs/label/index.tsx +85 -0
  69. package/components/inputs/number-input/examples.mdx +56 -0
  70. package/components/inputs/number-input/index.tsx +214 -0
  71. package/components/inputs/radio/examples.mdx +77 -0
  72. package/components/inputs/radio/index.tsx +636 -0
  73. package/components/inputs/search/examples.mdx +74 -0
  74. package/components/inputs/search/index.tsx +333 -0
  75. package/components/inputs/select/examples.mdx +218 -0
  76. package/components/inputs/select/index.tsx +862 -0
  77. package/components/inputs/select/variants.ts +32 -0
  78. package/components/inputs/slider/examples.mdx +127 -0
  79. package/components/inputs/slider/index.tsx +1725 -0
  80. package/components/inputs/switch/examples.mdx +94 -0
  81. package/components/inputs/switch/index.tsx +504 -0
  82. package/components/inputs/textarea/examples.mdx +88 -0
  83. package/components/inputs/textarea/index.tsx +161 -0
  84. package/components/navigation/navigation-menu/examples.mdx +109 -0
  85. package/components/navigation/navigation-menu/index.tsx +613 -0
  86. package/components/navigation/pagination/examples.mdx +64 -0
  87. package/components/navigation/pagination/index.tsx +395 -0
  88. package/components/navigation/sidebar/examples.mdx +319 -0
  89. package/components/navigation/sidebar/index.tsx +453 -0
  90. package/components/navigation/sidebar/sidebar-core.tsx +1408 -0
  91. package/components/navigation/sidebar/sidebar-menu.tsx +1495 -0
  92. package/components/navigation/sidebar/sidebar.css +74 -0
  93. package/components/navigation/sidebar/variants.ts +55 -0
  94. package/components/navigation/tabs/examples.mdx +89 -0
  95. package/components/navigation/tabs/index.tsx +494 -0
  96. package/components/navigation/tabs-subtle/examples.mdx +49 -0
  97. package/components/navigation/tabs-subtle/index.tsx +512 -0
  98. package/components/overlays/alert-dialog/examples.mdx +123 -0
  99. package/components/overlays/alert-dialog/index.tsx +258 -0
  100. package/components/overlays/alert-dialog/pending-demo.tsx +36 -0
  101. package/components/overlays/command/examples.mdx +113 -0
  102. package/components/overlays/command/index.tsx +416 -0
  103. package/components/overlays/context-menu/examples.mdx +56 -0
  104. package/components/overlays/context-menu/index.tsx +279 -0
  105. package/components/overlays/dialog/examples.mdx +144 -0
  106. package/components/overlays/dialog/index.tsx +366 -0
  107. package/components/overlays/dropdown/dropdown-search.tsx +395 -0
  108. package/components/overlays/dropdown/examples.mdx +294 -0
  109. package/components/overlays/dropdown/index.tsx +1168 -0
  110. package/components/overlays/dropdown/menu-item.tsx +464 -0
  111. package/components/overlays/dropdown/menu-parts.tsx +660 -0
  112. package/components/overlays/popover/anchored-demo.tsx +59 -0
  113. package/components/overlays/popover/examples.mdx +180 -0
  114. package/components/overlays/popover/index.tsx +315 -0
  115. package/components/overlays/preview-card/examples.mdx +61 -0
  116. package/components/overlays/preview-card/index.tsx +204 -0
  117. package/components/overlays/sheet/examples.mdx +89 -0
  118. package/components/overlays/sheet/index.tsx +247 -0
  119. package/components/overlays/toast/examples.mdx +68 -0
  120. package/components/overlays/toast/global-demo.tsx +72 -0
  121. package/components/overlays/toast/index.tsx +474 -0
  122. package/components/overlays/toast/manager.ts +193 -0
  123. package/components/overlays/tooltip/examples.mdx +106 -0
  124. package/components/overlays/tooltip/index.tsx +519 -0
  125. package/components/progress/examples.mdx +59 -0
  126. package/components/progress/index.tsx +174 -0
  127. package/components/progress/progress.css +49 -0
  128. package/components/progress/step-demo.tsx +27 -0
  129. package/components/table/examples.mdx +184 -0
  130. package/components/table/index.tsx +510 -0
  131. package/components/table/sort-demo.tsx +61 -0
  132. package/dist/base.css +100 -0
  133. package/dist/blocks/sidebar-app/index.js +39 -26
  134. package/dist/components/AI/input-message/index.js +22 -16
  135. package/dist/components/AI/thinking-indicator/index.js +9 -6
  136. package/dist/components/accordion/index.js +16 -13
  137. package/dist/components/avatar/index.d.ts +46 -0
  138. package/dist/components/avatar/index.js +18 -0
  139. package/dist/components/badge/index.d.ts +18 -11
  140. package/dist/components/badge/index.js +11 -7
  141. package/dist/components/badge/variants.d.ts +33 -0
  142. package/dist/components/badge/variants.js +10 -0
  143. package/dist/components/button/index.d.ts +53 -17
  144. package/dist/components/button/index.js +10 -6
  145. package/dist/components/button/variants.d.ts +39 -0
  146. package/dist/components/button/variants.js +8 -0
  147. package/dist/components/card/index.d.ts +16 -3
  148. package/dist/components/card/index.js +16 -11
  149. package/dist/components/copy/index.js +13 -9
  150. package/dist/components/inputs/checkbox/index.d.ts +28 -9
  151. package/dist/components/inputs/checkbox/index.js +17 -13
  152. package/dist/components/inputs/color-picker/index.d.ts +13 -13
  153. package/dist/components/inputs/color-picker/index.js +25 -18
  154. package/dist/components/inputs/color-picker/variants.d.ts +61 -0
  155. package/dist/components/inputs/color-picker/variants.js +28 -0
  156. package/dist/components/inputs/combobox/index.d.ts +60 -17
  157. package/dist/components/inputs/combobox/index.js +26 -18
  158. package/dist/components/inputs/date-time/calendar/index.js +14 -11
  159. package/dist/components/inputs/date-time/date-picker/index.d.ts +53 -11
  160. package/dist/components/inputs/date-time/date-picker/index.js +33 -25
  161. package/dist/components/inputs/date-time/date-range-picker/index.d.ts +3 -3
  162. package/dist/components/inputs/date-time/date-range-picker/index.js +32 -24
  163. package/dist/components/inputs/date-time/duration-picker/index.d.ts +3 -3
  164. package/dist/components/inputs/date-time/duration-picker/index.js +32 -24
  165. package/dist/components/inputs/date-time/month-picker/index.d.ts +3 -3
  166. package/dist/components/inputs/date-time/month-picker/index.js +31 -23
  167. package/dist/components/inputs/date-time/time-picker/index.d.ts +3 -3
  168. package/dist/components/inputs/date-time/time-picker/index.js +32 -24
  169. package/dist/components/inputs/field/index.d.ts +116 -0
  170. package/dist/components/inputs/field/index.js +28 -0
  171. package/dist/components/inputs/file-dropzone/index.d.ts +165 -0
  172. package/dist/components/inputs/file-dropzone/index.js +32 -0
  173. package/dist/components/inputs/file-dropzone/variants.d.ts +35 -0
  174. package/dist/components/inputs/file-dropzone/variants.js +12 -0
  175. package/dist/components/inputs/input/index.d.ts +95 -13
  176. package/dist/components/inputs/input/index.js +17 -9
  177. package/dist/components/inputs/label/index.d.ts +32 -0
  178. package/dist/components/inputs/label/index.js +11 -0
  179. package/dist/components/inputs/number-input/index.d.ts +84 -0
  180. package/dist/components/inputs/number-input/index.js +20 -0
  181. package/dist/components/inputs/radio/index.d.ts +20 -5
  182. package/dist/components/inputs/radio/index.js +15 -12
  183. package/dist/components/inputs/search/index.js +25 -18
  184. package/dist/components/inputs/select/index.d.ts +41 -11
  185. package/dist/components/inputs/select/index.js +27 -14
  186. package/dist/components/inputs/select/variants.d.ts +5 -0
  187. package/dist/components/inputs/select/variants.js +6 -0
  188. package/dist/components/inputs/slider/index.js +7 -5
  189. package/dist/components/inputs/switch/index.d.ts +8 -2
  190. package/dist/components/inputs/switch/index.js +14 -12
  191. package/dist/components/inputs/textarea/index.d.ts +4 -0
  192. package/dist/components/inputs/textarea/index.js +8 -6
  193. package/dist/components/navigation/navigation-menu/index.d.ts +114 -0
  194. package/dist/components/navigation/navigation-menu/index.js +40 -0
  195. package/dist/components/navigation/pagination/index.d.ts +73 -0
  196. package/dist/components/navigation/pagination/index.js +39 -0
  197. package/dist/components/navigation/sidebar/index.d.ts +44 -21
  198. package/dist/components/navigation/sidebar/index.js +41 -31
  199. package/dist/components/navigation/sidebar/variants.d.ts +25 -0
  200. package/dist/components/navigation/sidebar/variants.js +28 -0
  201. package/dist/components/navigation/tabs/index.d.ts +8 -2
  202. package/dist/components/navigation/tabs/index.js +14 -10
  203. package/dist/components/navigation/tabs-subtle/index.js +12 -10
  204. package/dist/components/overlays/alert-dialog/index.d.ts +121 -15
  205. package/dist/components/overlays/alert-dialog/index.js +22 -13
  206. package/dist/components/overlays/command/index.js +24 -17
  207. package/dist/components/overlays/context-menu/index.d.ts +33 -17
  208. package/dist/components/overlays/context-menu/index.js +27 -18
  209. package/dist/components/overlays/dialog/index.js +17 -12
  210. package/dist/components/overlays/dropdown/index.d.ts +257 -31
  211. package/dist/components/overlays/dropdown/index.js +42 -17
  212. package/dist/components/overlays/popover/index.d.ts +31 -9
  213. package/dist/components/overlays/popover/index.js +12 -9
  214. package/dist/components/overlays/preview-card/index.js +11 -8
  215. package/dist/components/overlays/sheet/index.js +18 -13
  216. package/dist/components/overlays/toast/index.d.ts +123 -47
  217. package/dist/components/overlays/toast/index.js +18 -10
  218. package/dist/components/overlays/tooltip/index.d.ts +113 -21
  219. package/dist/components/overlays/tooltip/index.js +15 -6
  220. package/dist/components/progress/index.d.ts +56 -0
  221. package/dist/components/progress/index.js +15 -0
  222. package/dist/components/table/index.d.ts +63 -8
  223. package/dist/components/table/index.js +19 -9
  224. package/dist/index.d.ts +2201 -736
  225. package/dist/index.js +223 -82
  226. package/dist/lib/date-locale.d.ts +14 -0
  227. package/dist/lib/date-locale.js +16 -0
  228. package/dist/lib/field-classes.d.ts +144 -0
  229. package/dist/lib/field-classes.js +40 -0
  230. package/dist/lib/field-variants.d.ts +47 -0
  231. package/dist/lib/field-variants.js +26 -0
  232. package/dist/lib/fluid-hover-highlight.d.ts +112 -0
  233. package/dist/lib/fluid-hover-highlight.js +17 -0
  234. package/dist/lib/focus-ring.d.ts +18 -0
  235. package/dist/lib/focus-ring.js +10 -0
  236. package/dist/lib/goo-indicator.d.ts +46 -0
  237. package/dist/lib/goo-indicator.js +14 -0
  238. package/dist/lib/icon-context.d.ts +30 -0
  239. package/dist/lib/icon-context.js +12 -0
  240. package/dist/lib/inert.d.ts +9 -0
  241. package/dist/lib/inert.js +8 -0
  242. package/dist/lib/morph-layers.d.ts +93 -0
  243. package/dist/lib/morph-layers.js +13 -0
  244. package/dist/lib/morph-part.d.ts +21 -0
  245. package/dist/lib/morph-part.js +12 -0
  246. package/dist/lib/popup.d.ts +20 -0
  247. package/dist/lib/popup.js +12 -0
  248. package/dist/lib/reduced-motion.d.ts +9 -0
  249. package/dist/lib/reduced-motion.js +6 -0
  250. package/dist/lib/row-index.d.ts +10 -0
  251. package/dist/lib/row-index.js +8 -0
  252. package/dist/lib/row-order.d.ts +28 -0
  253. package/dist/lib/row-order.js +10 -0
  254. package/dist/lib/scroll-column.d.ts +27 -0
  255. package/dist/lib/scroll-column.js +18 -0
  256. package/dist/lib/shape-context.d.ts +30 -0
  257. package/dist/lib/shape-context.js +12 -0
  258. package/dist/lib/size-context.d.ts +94 -0
  259. package/dist/lib/size-context.js +20 -0
  260. package/dist/lib/size-scale.d.ts +79 -0
  261. package/dist/lib/size-scale.js +8 -0
  262. package/dist/lib/slot.d.ts +12 -0
  263. package/dist/lib/slot.js +6 -0
  264. package/dist/lib/springs.d.ts +44 -0
  265. package/dist/lib/springs.js +8 -0
  266. package/dist/lib/surface-classes.d.ts +19 -0
  267. package/dist/lib/surface-classes.js +10 -0
  268. package/dist/lib/surface-context.d.ts +9 -0
  269. package/dist/lib/surface-context.js +8 -0
  270. package/dist/lib/use-controllable-state.d.ts +2 -0
  271. package/dist/lib/use-controllable-state.js +6 -0
  272. package/dist/lib/use-fluid-hover.d.ts +138 -0
  273. package/dist/lib/use-fluid-hover.js +14 -0
  274. package/dist/lib/use-is-mobile.d.ts +2 -0
  275. package/dist/lib/use-is-mobile.js +6 -0
  276. package/dist/lib/use-keyboard-nav-gate.d.ts +20 -0
  277. package/dist/lib/use-keyboard-nav-gate.js +7 -0
  278. package/dist/lib/use-merge-split.d.ts +47 -0
  279. package/dist/lib/use-merge-split.js +15 -0
  280. package/dist/lib/use-morph.d.ts +101 -0
  281. package/dist/lib/use-morph.js +20 -0
  282. package/dist/lib/use-touch-primary.d.ts +2 -0
  283. package/dist/lib/use-touch-primary.js +6 -0
  284. package/dist/lib/utils.d.ts +3 -0
  285. package/dist/lib/utils.js +6 -0
  286. package/dist/preset.d.ts +17 -0
  287. package/dist/preset.js +22 -2
  288. package/dist/primitives/code/index.js +7 -5
  289. package/dist/primitives/craft/index.js +9 -6
  290. package/dist/primitives/fluid-hover/index.js +9 -8
  291. package/dist/primitives/morph/index.js +15 -10
  292. package/dist/primitives/motion/index.js +10 -8
  293. package/dist/primitives/scroll-area/index.js +4 -3
  294. package/dist/primitives/separator/index.d.ts +25 -0
  295. package/dist/primitives/separator/index.js +9 -0
  296. package/dist/primitives/sizes/index.js +6 -4
  297. package/dist/primitives/skeleton/index.d.ts +35 -0
  298. package/dist/primitives/skeleton/index.js +12 -0
  299. package/dist/primitives/surface/index.js +6 -4
  300. package/dist/shared/{components-3mkepka5.js → components-0jmqhx2k.js} +28 -25
  301. package/dist/shared/components-124epb7f.js +53 -0
  302. package/dist/shared/{components-gz8zf3ec.js → components-1k0wv53d.js} +15 -43
  303. package/dist/shared/components-26snnbs9.js +322 -0
  304. package/dist/shared/{components-2dxkv6zm.js → components-2c0qcfx4.js} +8 -7
  305. package/dist/shared/{components-473g814w.js → components-2mt9te6d.js} +243 -137
  306. package/dist/shared/{components-hawvy2qz.js → components-2z48g4fp.js} +9 -30
  307. package/dist/shared/components-34txk9rj.js +35 -0
  308. package/dist/shared/components-3h92d94m.js +23 -0
  309. package/dist/shared/{components-zfnmn5d4.js → components-3sp623ae.js} +36 -31
  310. package/dist/shared/{components-jwst3vtj.js → components-3wxkjgqn.js} +186 -76
  311. package/dist/shared/{components-s2bz1xpk.js → components-49bsynbw.js} +8 -5
  312. package/dist/shared/components-4bwsd46w.js +71 -0
  313. package/dist/shared/components-4n4f5m3e.js +297 -0
  314. package/dist/shared/{components-0q0sqfh4.js → components-4y816hwp.js} +27 -49
  315. package/dist/shared/components-52ngsgca.js +399 -0
  316. package/dist/shared/{components-y9y0g8er.js → components-52r5763g.js} +22 -19
  317. package/dist/shared/components-559tf7yb.js +28 -0
  318. package/dist/shared/components-5mkga9hj.js +440 -0
  319. package/dist/shared/{components-kwa6mrf9.js → components-5s0nfqvw.js} +14 -9
  320. package/dist/shared/{components-dk0gjr5a.js → components-60atskhp.js} +19 -18
  321. package/dist/shared/{components-rr7a6ja9.js → components-658b26nb.js} +21 -18
  322. package/dist/shared/{components-xxvdw7k1.js → components-65vwfjs6.js} +37 -34
  323. package/dist/shared/{components-frw34b65.js → components-6j2fh74k.js} +114 -123
  324. package/dist/shared/components-6zg1yft7.js +167 -0
  325. package/dist/shared/components-6zng858q.js +53 -0
  326. package/dist/shared/components-72w4ge68.js +64 -0
  327. package/dist/shared/{components-cv16mryv.js → components-78x40gee.js} +13 -7
  328. package/dist/shared/{components-b2wyq2cq.js → components-7d4r7hh1.js} +39 -57
  329. package/dist/shared/components-7ntvvegr.js +316 -0
  330. package/dist/shared/components-8nwsqjn8.js +34 -0
  331. package/dist/shared/{components-5xc2j8jz.js → components-8tt3xbb5.js} +19 -18
  332. package/dist/shared/{components-vc7vq5nz.js → components-9as3rf68.js} +6 -3
  333. package/dist/shared/components-9mys9jna.js +255 -0
  334. package/dist/shared/components-adkarj8n.js +35 -0
  335. package/dist/shared/{components-b3c5et5b.js → components-ae9fncz4.js} +32 -31
  336. package/dist/shared/{components-hqeq3w9q.js → components-awra5sff.js} +24 -16
  337. package/dist/shared/components-b1e239g3.js +28 -0
  338. package/dist/shared/{components-q5xj5c16.js → components-b1f2bjf1.js} +9 -8
  339. package/dist/shared/{components-qmmjx1qw.js → components-bn42vq6d.js} +71 -72
  340. package/dist/shared/components-c96g82th.js +1444 -0
  341. package/dist/shared/components-c9yr4ed1.js +44 -0
  342. package/dist/shared/components-d3shbwqq.js +95 -0
  343. package/dist/shared/{components-jjw69c4c.js → components-dcc02hs2.js} +7 -4
  344. package/dist/shared/{components-men63rwj.js → components-de99ykt7.js} +3 -2
  345. package/dist/shared/components-dhgk6mbt.js +16 -0
  346. package/dist/shared/components-drxrtnp2.js +25 -0
  347. package/dist/shared/{components-6p8tfcd6.js → components-ds9fqat9.js} +21 -18
  348. package/dist/shared/{components-mjthm1kf.js → components-egy4tyjt.js} +11 -8
  349. package/dist/shared/components-f0xfpkhh.js +82 -0
  350. package/dist/shared/components-f7h57m6t.js +59 -0
  351. package/dist/shared/{components-d6xtjht3.js → components-fjkpbg6n.js} +33 -30
  352. package/dist/shared/{components-gc5vtq2f.js → components-gh0qeey3.js} +63 -29
  353. package/dist/shared/{components-39jq9va1.js → components-gvbapfen.js} +29 -24
  354. package/dist/shared/{components-5vgpnxgy.js → components-gzesg7hy.js} +19 -16
  355. package/dist/shared/components-h6p86be3.js +19 -0
  356. package/dist/shared/components-h83az0bz.js +33 -0
  357. package/dist/shared/components-jjrj6ymn.js +104 -0
  358. package/dist/shared/{components-wvp1m52g.js → components-jp5v0m0f.js} +15 -14
  359. package/dist/shared/{components-zrkzcayz.js → components-jqpefjsn.js} +54 -6
  360. package/dist/shared/{components-1z6tdadk.js → components-k74j4hwh.js} +20 -17
  361. package/dist/shared/components-kswxnm3h.js +24 -0
  362. package/dist/shared/{components-jbbmdr74.js → components-mkkbbgb7.js} +36 -17
  363. package/dist/shared/{components-pbmqctfa.js → components-mpw667rb.js} +169 -65
  364. package/dist/shared/{components-s5ngg32c.js → components-paa08427.js} +121 -47
  365. package/dist/shared/{components-vfbhxf5r.js → components-pd42emw3.js} +6 -5
  366. package/dist/shared/{components-hw9yk32z.js → components-pmfszv7a.js} +35 -23
  367. package/dist/shared/{components-chacnk61.js → components-pw06rbxq.js} +19 -19
  368. package/dist/shared/components-q03cktx8.js +246 -0
  369. package/dist/shared/{components-kvr7f9hy.js → components-qdm3yh26.js} +19 -10
  370. package/dist/shared/{components-27nnvrwe.js → components-qs4rkqsj.js} +15 -14
  371. package/dist/shared/{components-s083f38z.js → components-r6gbmkew.js} +22 -21
  372. package/dist/shared/{components-ta7shw28.js → components-r8eqs7gz.js} +4 -4
  373. package/dist/shared/{components-sn1rxx1d.js → components-rm02h9nt.js} +12 -11
  374. package/dist/shared/{components-c6s6b43g.js → components-rrbpmfx4.js} +3 -2
  375. package/dist/shared/{components-n7mjr357.js → components-tz1fv35x.js} +3 -2
  376. package/dist/shared/components-tz4gaf1v.js +23 -0
  377. package/dist/shared/{components-zx6b6yby.js → components-vf45j12c.js} +4 -18
  378. package/dist/shared/{components-3ky44908.js → components-vfjc9jzz.js} +2 -3
  379. package/dist/shared/{components-39vmfq9s.js → components-vsa625vk.js} +21 -20
  380. package/dist/shared/components-wawnmkph.js +500 -0
  381. package/dist/shared/components-wktrrb4m.js +38 -0
  382. package/dist/shared/{components-8snj490h.js → components-wwd363me.js} +78 -32
  383. package/dist/shared/{components-5pnw8183.js → components-xcwvgcrd.js} +23 -22
  384. package/dist/shared/components-xdqmt6ed.js +20 -0
  385. package/dist/shared/{components-h2ajmhwn.js → components-y1w3a6wx.js} +107 -35
  386. package/dist/shared/{components-ar66gnt0.js → components-y6578b8h.js} +37 -34
  387. package/dist/shared/{components-59efxjd9.js → components-yhgq0qxx.js} +13 -12
  388. package/dist/shared/{components-jthe1pwb.js → components-yz84gpkq.js} +101 -404
  389. package/dist/shared/components-z3v0n0de.js +11 -0
  390. package/dist/shared/{components-7r3etddy.js → components-z9n19fyk.js} +19 -54
  391. package/dist/shared/components-zbvjzh1p.js +141 -0
  392. package/dist/styles.css +241 -16
  393. package/dist/tailwind.css +13 -0
  394. package/dist/tokens.css +34 -0
  395. package/index.ts +60 -0
  396. package/lib/date-locale.ts +50 -0
  397. package/lib/field-classes.ts +115 -0
  398. package/lib/field-variants.ts +117 -0
  399. package/lib/fluid-hover-highlight.tsx +139 -0
  400. package/lib/focus-ring.tsx +46 -0
  401. package/lib/goo-indicator.tsx +261 -0
  402. package/lib/icon-context.tsx +310 -0
  403. package/lib/inert.ts +22 -0
  404. package/lib/morph-layers.tsx +102 -0
  405. package/lib/morph-part.tsx +49 -0
  406. package/lib/popup.ts +52 -0
  407. package/lib/reduced-motion.ts +19 -0
  408. package/lib/row-index.tsx +29 -0
  409. package/lib/row-order.tsx +115 -0
  410. package/lib/scroll-column.tsx +244 -0
  411. package/lib/shape-context.tsx +124 -0
  412. package/lib/size-context.tsx +66 -0
  413. package/lib/size-scale.ts +115 -0
  414. package/lib/slot.ts +25 -0
  415. package/lib/springs.ts +62 -0
  416. package/lib/surface-classes.ts +58 -0
  417. package/lib/surface-context.tsx +21 -0
  418. package/lib/use-controllable-state.ts +44 -0
  419. package/lib/use-fluid-hover.ts +541 -0
  420. package/lib/use-is-mobile.ts +25 -0
  421. package/lib/use-keyboard-nav-gate.ts +45 -0
  422. package/lib/use-merge-split.tsx +209 -0
  423. package/lib/use-morph.ts +557 -0
  424. package/lib/use-touch-primary.tsx +43 -0
  425. package/lib/utils.ts +37 -0
  426. package/package.json +177 -4
  427. package/preset.ts +196 -0
  428. package/primitives/code/examples.mdx +27 -0
  429. package/primitives/code/index.tsx +80 -0
  430. package/primitives/craft/demos.tsx +173 -0
  431. package/primitives/craft/examples.mdx +138 -0
  432. package/primitives/craft/index.tsx +76 -0
  433. package/primitives/fluid-hover/examples.mdx +84 -0
  434. package/primitives/fluid-hover/index.tsx +316 -0
  435. package/primitives/morph/examples.mdx +135 -0
  436. package/primitives/morph/index.tsx +192 -0
  437. package/primitives/motion/examples.mdx +100 -0
  438. package/primitives/motion/index.tsx +160 -0
  439. package/primitives/scroll-area/examples.mdx +58 -0
  440. package/primitives/scroll-area/index.tsx +200 -0
  441. package/primitives/separator/examples.mdx +30 -0
  442. package/primitives/separator/index.tsx +53 -0
  443. package/primitives/sizes/examples.mdx +167 -0
  444. package/primitives/sizes/index.tsx +89 -0
  445. package/primitives/skeleton/examples.mdx +51 -0
  446. package/primitives/skeleton/index.tsx +72 -0
  447. package/primitives/skeleton/skeleton.css +15 -0
  448. package/primitives/surface/examples.mdx +173 -0
  449. package/primitives/surface/index.tsx +109 -0
  450. package/primitives/surface/surface.css +176 -0
  451. package/dist/shared/components-0sn7ypjw.js +0 -61
  452. package/dist/shared/components-1434p1mh.js +0 -123
  453. package/dist/shared/components-a86dytt0.js +0 -986
  454. package/dist/shared/components-gf5s6ap9.js +0 -42
  455. package/dist/shared/components-hyq23phv.js +0 -310
  456. package/dist/shared/components-ng72k97e.js +0 -128
  457. package/dist/shared/components-raknhppb.js +0 -64
  458. package/dist/shared/components-vynjtrk6.js +0 -19
  459. package/dist/shared/components-wy9jpqny.js +0 -146
  460. /package/dist/shared/{components-1v7rxytx.js → components-3ey0hzpb.js} +0 -0
  461. /package/dist/shared/{components-ezxhrmer.js → components-jcnjay5g.js} +0 -0
@@ -0,0 +1,164 @@
1
+ import Button, { buttonVariants } from './index'
2
+ import LoadingDemo from './loading-demo'
3
+ import { ArrowRight, Check, ChevronDown, Loader, Plus, RotateCcw, Search, Settings, Trash2 } from 'lucide-react'
4
+
5
+ # Variants
6
+
7
+ 4 variants, inline or full width: add `className="w-full"` to stretch one
8
+ to its container.
9
+
10
+ <div className="flex w-fit max-w-full flex-col gap-8">
11
+ <div className="flex items-center gap-2">
12
+ <Button variant="primary">Primary</Button>
13
+ <Button variant="secondary">Secondary</Button>
14
+ <Button variant="tertiary">Tertiary</Button>
15
+ <Button variant="ghost">Ghost</Button>
16
+ </div>
17
+ <div className="flex flex-col gap-2">
18
+ <Button variant="primary" className="w-full">Primary</Button>
19
+ <Button variant="secondary" className="w-full">Secondary</Button>
20
+ <Button variant="tertiary" className="w-full">Tertiary</Button>
21
+ <Button variant="ghost" className="w-full">Ghost</Button>
22
+ </div>
23
+ </div>
24
+
25
+ # Destructive and success
26
+
27
+ A status as an action: its hue as the label on a wash of itself, deepening on
28
+ hover and press. Never a solid fill, so the view keeps one primary.
29
+
30
+ <div className="flex flex-wrap items-center gap-2">
31
+ <Button variant="destructive" leadingIcon={Trash2}>Delete</Button>
32
+ <Button variant="success" leadingIcon={Check}>Approve</Button>
33
+ <Button variant="destructive" size="compact">Remove</Button>
34
+ <Button variant="success" size="compact">Accept</Button>
35
+ </div>
36
+
37
+ # Outline and link
38
+
39
+ `outline` is `tertiary` under its shadcn name. `link` is text on the control
40
+ height: its underline fades in on hover, so the label never moves, and it
41
+ lines up with the buttons beside it.
42
+
43
+ <div className="flex flex-wrap items-center gap-2">
44
+ <Button variant="outline">Outline</Button>
45
+ <Button variant="link">Forgot password?</Button>
46
+ <Button variant="link" trailingIcon={ArrowRight}>View all</Button>
47
+ </div>
48
+
49
+ # With Icons
50
+
51
+ `leadingIcon` and `trailingIcon` take an icon component; it sits 4px
52
+ closer to its edge than the label would.
53
+
54
+ <div className="flex flex-wrap items-center gap-2">
55
+ <Button leadingIcon={Plus}>Create</Button>
56
+ <Button variant="secondary" trailingIcon={ArrowRight}>Next</Button>
57
+ <Button variant="tertiary" leadingIcon={Search} trailingIcon={ArrowRight}>
58
+ Search
59
+ </Button>
60
+ </div>
61
+
62
+ # Loading & Disabled
63
+
64
+ `loading` folds the surface into a circle around a spinner and disables
65
+ the button; the hidden label keeps its width.
66
+
67
+ <div className="flex flex-wrap items-center gap-2">
68
+ <Button loading>Loading</Button>
69
+ <Button variant="secondary" loading leadingIcon={Loader}>Saving</Button>
70
+ <Button disabled>Disabled</Button>
71
+ </div>
72
+
73
+ # Loading morph
74
+
75
+ Press one. The surface folds into a circle around the spinner while the
76
+ button keeps its box, so its neighbors never move, and springs back when
77
+ loading ends. It keeps focus while it loads (`aria-disabled`, not
78
+ `disabled`), so press one with Enter and focus is still there when it is
79
+ done; presses in between are ignored. `LoadingDemo` is a docs stage: a
80
+ Button whose `loading` holds for two seconds after a press.
81
+
82
+ <div className="flex w-fit max-w-full flex-col gap-3">
83
+ <div className="flex flex-wrap items-center gap-2">
84
+ <LoadingDemo leadingIcon={Check}>Save</LoadingDemo>
85
+ <LoadingDemo variant="secondary">Publish</LoadingDemo>
86
+ <LoadingDemo variant="tertiary" trailingIcon={ArrowRight}>Next</LoadingDemo>
87
+ <LoadingDemo variant="secondary" size="icon" aria-label="Refresh">
88
+ <RotateCcw />
89
+ </LoadingDemo>
90
+ </div>
91
+ <LoadingDemo className="w-full">Create account</LoadingDemo>
92
+ </div>
93
+
94
+ # Sizes
95
+
96
+ The size ladder: `default` (36px) and `compact` (28px), plus square
97
+ icon-only steps. There is no third height: `xs` and `sm` resolve to `compact`. Icon-only buttons take the icon as their child and need
98
+ an `aria-label`.
99
+
100
+ <div className="flex flex-col gap-3">
101
+ <div className="flex flex-wrap items-center gap-2">
102
+ <Button leadingIcon={Plus}>Default</Button>
103
+ <Button variant="secondary" size="icon" aria-label="Add">
104
+ <Plus />
105
+ </Button>
106
+ <Button variant="ghost" size="icon" aria-label="Settings">
107
+ <Settings />
108
+ </Button>
109
+ </div>
110
+ <div className="flex flex-wrap items-center gap-2">
111
+ <Button size="compact" leadingIcon={Plus}>Compact</Button>
112
+ <Button variant="secondary" size="icon-compact" aria-label="Add">
113
+ <Plus />
114
+ </Button>
115
+ <Button variant="ghost" size="icon-compact" aria-label="Settings">
116
+ <Settings />
117
+ </Button>
118
+ </div>
119
+ </div>
120
+
121
+ # Active
122
+
123
+ `active` forces the pressed visual — for a button whose dropdown or
124
+ popover is currently open.
125
+
126
+ <div className="flex flex-wrap items-center gap-2">
127
+ <Button active>Primary</Button>
128
+ <Button variant="secondary" active>Secondary</Button>
129
+ <Button variant="tertiary" active trailingIcon={ChevronDown}>Tertiary</Button>
130
+ <Button variant="ghost" active>Ghost</Button>
131
+ </div>
132
+
133
+ # As child
134
+
135
+ `asChild` merges the button's styling onto its single child element —
136
+ here a link — instead of rendering a `<button>`.
137
+
138
+ <Button asChild variant="tertiary" trailingIcon={ArrowRight}>
139
+ <a href="https://www.fluidfunctionalism.com" target="_blank" rel="noreferrer">
140
+ Fluid Functionalism
141
+ </a>
142
+ </Button>
143
+
144
+ # Surface classes
145
+
146
+ For an element you can't render through `asChild` (a router's link that
147
+ builds its own anchor, markup from a server template), `buttonVariants`
148
+ with `surface: true` draws the fill, ring, hover, press and focus ring on
149
+ the element itself. Without `surface` it returns only the root classes, and
150
+ the fill lives on the Button's inner layer. It is static: no press shrink,
151
+ no loading morph, so prefer `<Button asChild>` where you can. Size aliases
152
+ (`sm`, `lg`, `icon-sm`, …) resolve as on the Button; `aria-disabled` dims it.
153
+
154
+ <div className="flex flex-wrap items-center gap-2">
155
+ <a href="#surface-classes" className={buttonVariants({ surface: true })}>Primary</a>
156
+ <a href="#surface-classes" className={buttonVariants({ variant: 'secondary', surface: true })}>Secondary</a>
157
+ <a href="#surface-classes" className={buttonVariants({ variant: 'outline', surface: true })}>Outline</a>
158
+ <a href="#surface-classes" className={buttonVariants({ variant: 'ghost', surface: true })}>Ghost</a>
159
+ <a href="#surface-classes" className={buttonVariants({ variant: 'destructive', size: 'sm', surface: true })}>Remove</a>
160
+ <a href="#surface-classes" className={buttonVariants({ variant: 'secondary', size: 'icon-sm', surface: true })} aria-label="Add">
161
+ <Plus />
162
+ </a>
163
+ <a href="#surface-classes" aria-disabled="true" className={buttonVariants({ variant: 'secondary', surface: true })}>Disabled</a>
164
+ </div>
@@ -0,0 +1,416 @@
1
+ // biome-ignore-all lint: vendored upstream code keeps its own patterns (see the header)
2
+ /*
3
+ * Ported from Fluid Functionalism — github.com/mickadesign/fluid-functionalism
4
+ * `registry/base/button.tsx` @ b3587bdbd83fc66c2a6aae3817ffb856cb09260b
5
+ * MIT License © 2026 Micka Touillaud — fluidfunctionalism.com (notice: LICENSE.fluid-functionalism)
6
+ * Local modifications:
7
+ * - `"use client"` directive dropped (no RSC here).
8
+ * - `@/lib/{icon-context,utils,shape-context,size-context}` imports rewritten to `../../lib/*`.
9
+ * - `ButtonProps`: `variant`, `disabled` and `children` re-declared in the interface body (modo's
10
+ * parser lists only members declared there); member docs replaced by the FF docs API-table text
11
+ * (upstream's size-alias and `active` notes folded in).
12
+ * - modo item: TSDoc (from the FF "Button" docs page, plus Sizes / Active / As child examples for
13
+ * the API-table props the page has no section for) and a default export.
14
+ * - Also the modo docs-chrome Button (`modo.config.ts` shell.Button): the chrome renders its
15
+ * icon buttons as `<Button variant="ghost" size="icon-sm">`, served by upstream's legacy
16
+ * `icon-sm` → `icon-compact` alias.
17
+ * - Styling reads DS tokens (AGENTS.md styling): `text-[Npx]` →
18
+ * `text-<role>[-compact]`; the hex focus-ring fallback →
19
+ * `ring-focus-ring` / `border-focus-ring`; `duration-80|120|160` and
20
+ * tier-length JS durations → `duration-<tier>` / `spring.*`.
21
+ * - `loading` morphs (local, in the morph layer's language): the surface layer contracts into
22
+ * a circle around the spinner on `spring.moderate` and springs back when loading ends, while
23
+ * the button's own box keeps its size; the label and the spinner crossfade instead of
24
+ * swapping. The spinner centres on the button, not on the label. `aria-busy` while loading.
25
+ * The root's `transition-colors` → `transition-[color,opacity]`, so the disabled dim fades
26
+ * in step instead of jumping (the root draws no background or border of its own).
27
+ * A loading button stays focusable (Base UI's `focusableWhenDisabled`, so
28
+ * `aria-disabled`, not `disabled`) and ignores presses; with `asChild` it
29
+ * sets `aria-disabled` and swallows clicks itself.
30
+ * - `buttonVariants` and the size aliases moved to `variants.ts` (server-callable, no
31
+ * JSX or hooks) and re-exported from here; it resolves the size aliases itself and
32
+ * takes `surface` (the fill as root classes, for an anchor host; see `variants.ts`).
33
+ * - Variants added for hosts with a shadcn-style API (local, under DESIGN.md): `outline`
34
+ * renders exactly as `tertiary` (both are the transparent 1px-ring button);
35
+ * `destructive` and `success` are their hue as text (`--destructive-strong`,
36
+ * `--success-strong`) on a translucent wash of it that deepens on hover and press,
37
+ * never a solid fill; `link` is foreground text on the control height with no padding,
38
+ * whose always-drawn underline fades in on hover (and shows while `active`). Size `xs`
39
+ * resolves to `compact`: the ladder has two steps.
40
+ */
41
+
42
+ import { Button as ButtonPrimitive } from '@base-ui/react/button'
43
+ import type { VariantProps } from 'class-variance-authority'
44
+ import { AnimatePresence, animate, motion, useMotionValue } from 'motion/react'
45
+ import {
46
+ type ButtonHTMLAttributes,
47
+ cloneElement,
48
+ forwardRef,
49
+ isValidElement,
50
+ type ReactElement,
51
+ type ReactNode,
52
+ useLayoutEffect,
53
+ useRef,
54
+ } from 'react'
55
+ import type { IconComponent } from '../../lib/icon-context'
56
+ import { useReduceMotion } from '../../lib/reduced-motion'
57
+ import { useShape } from '../../lib/shape-context'
58
+ import { useSizeVariant } from '../../lib/size-context'
59
+ import { spring } from '../../lib/springs'
60
+ import { cn } from '../../lib/utils'
61
+ import {
62
+ type ButtonSize,
63
+ type ButtonSizeCanonical,
64
+ type ButtonVariant,
65
+ buttonVariants,
66
+ legacySizeAliases,
67
+ } from './variants'
68
+
69
+ interface ButtonProps
70
+ extends ButtonHTMLAttributes<HTMLButtonElement>,
71
+ Omit<VariantProps<typeof buttonVariants>, 'size' | 'surface'> {
72
+ /** Visual style of the button. `outline` is `tertiary` under its shadcn name; `destructive` and `success` tint the label and a wash of their hue; `link` is underlined text on the control height. Defaults to `"primary"`. */
73
+ variant?: 'primary' | 'secondary' | 'tertiary' | 'outline' | 'ghost' | 'destructive' | 'success' | 'link'
74
+ /** Step on the size ladder (36px default, 28px compact — see Sizes): `"default"` | `"compact"` | `"icon"` | `"icon-compact"`. Omitted, the button follows the surrounding SizeProvider. Aliases resolve onto the ladder (xs, sm → compact; md/lg → default; icon-sm → icon-compact; icon-lg → icon). */
75
+ size?: 'default' | 'compact' | 'icon' | 'icon-compact' | 'xs' | 'sm' | 'md' | 'lg' | 'icon-sm' | 'icon-lg'
76
+ /** Merge props onto the child element instead of rendering a `<button>` — the single React-element child becomes the rendered element (slot-style). Defaults to `false`. */
77
+ asChild?: boolean
78
+ /** Folds the button into a circle around a spinner and blocks presses; the button keeps its box, so nothing around it moves, and keeps focus (`aria-disabled`, not `disabled`). Defaults to `false`. */
79
+ loading?: boolean
80
+ /** Icon displayed before the label. */
81
+ leadingIcon?: IconComponent
82
+ /** Icon displayed after the label. */
83
+ trailingIcon?: IconComponent
84
+ /** Forces the pressed/held visual — e.g. while a dropdown or popover the button opened is showing. Defaults to `false`. */
85
+ active?: boolean
86
+ /** Disables the button. Defaults to `false`. */
87
+ disabled?: boolean
88
+ /** The label. With `size="icon"` / `"icon-compact"`, the icon itself (give the button an `aria-label`). */
89
+ children?: ReactNode
90
+ }
91
+
92
+ /* Press effect: the surface layer sits 1px inside the button and a
93
+ same-color box-shadow spread fills it back out to the full bounds.
94
+ Pressing collapses the spread, shrinking the surface by exactly 1px per
95
+ side at any width — a scale would warp (2% of a 400px button is 8px
96
+ sideways but under 1px vertically). Fill colors are opaque color-mix()es
97
+ rather than alpha so the fill and its spread ring never seam. */
98
+ const bgVariants: Record<string, string> = {
99
+ primary:
100
+ '[--btn-bg:var(--foreground)] group-hover:[--btn-bg:color-mix(in_oklab,var(--foreground)_90%,var(--background))] group-active:[--btn-bg:color-mix(in_oklab,var(--foreground)_80%,var(--background))] bg-[var(--btn-bg)] shadow-[0_0_0_1px_var(--btn-bg)] group-active:shadow-[0_0_0_0px_var(--btn-bg)]',
101
+ secondary:
102
+ '[--btn-bg:var(--accent)] group-hover:[--btn-bg:color-mix(in_oklab,var(--accent)_80%,var(--background))] group-active:[--btn-bg:var(--accent)] bg-[var(--btn-bg)] shadow-[0_0_0_1px_var(--btn-bg)] group-active:shadow-[0_0_0_0px_var(--btn-bg)]',
103
+ // The border ring is an outer 1px shadow at rest that hands off to an
104
+ // inset 1px shadow when pressed, so the ring moves inward with the
105
+ // surface. The translucent fill only ever reaches the ring's inner edge
106
+ // (exactly the surface box), so it needs no spread of its own.
107
+ tertiary:
108
+ 'bg-transparent shadow-[0_0_0_1px_var(--border),inset_0_0_0_0px_var(--border)] group-hover:bg-hover group-active:bg-active group-active:shadow-[0_0_0_0px_var(--border),inset_0_0_0_1px_var(--border)]',
109
+ // Translucent fill + same-color spread never double up: outer shadows
110
+ // render only outside the surface box.
111
+ ghost:
112
+ 'bg-transparent shadow-[0_0_0_1px_transparent] group-hover:bg-hover group-hover:shadow-[0_0_0_1px_var(--hover)] group-active:bg-active group-active:shadow-[0_0_0_0px_var(--active)]',
113
+ // A status as an action: a translucent wash of its hue (the ghost recipe),
114
+ // deepening on hover and press.
115
+ // The fill reads the wash utilities; `--btn-bg` only feeds the spread.
116
+ destructive:
117
+ 'bg-destructive-wash group-hover:bg-destructive-wash-hover group-active:bg-destructive-wash-active [--btn-bg:var(--destructive-wash)] group-hover:[--btn-bg:var(--destructive-wash-hover)] group-active:[--btn-bg:var(--destructive-wash-active)] shadow-[0_0_0_1px_var(--btn-bg)] group-active:shadow-[0_0_0_0px_var(--btn-bg)]',
118
+ success:
119
+ 'bg-success-wash group-hover:bg-success-wash-hover group-active:bg-success-wash-active [--btn-bg:var(--success-wash)] group-hover:[--btn-bg:var(--success-wash-hover)] group-active:[--btn-bg:var(--success-wash-active)] shadow-[0_0_0_1px_var(--btn-bg)] group-active:shadow-[0_0_0_0px_var(--btn-bg)]',
120
+ link: 'bg-transparent',
121
+ }
122
+
123
+ /* Forced-active (`active` prop): pressed colors at full size; the
124
+ geometric press-collapse still reacts on top. */
125
+ const activeBgVariants: Record<string, string> = {
126
+ primary:
127
+ '[--btn-bg:color-mix(in_oklab,var(--foreground)_80%,var(--background))] bg-[var(--btn-bg)] shadow-[0_0_0_1px_var(--btn-bg)] group-active:shadow-[0_0_0_0px_var(--btn-bg)]',
128
+ secondary:
129
+ '[--btn-bg:var(--accent)] bg-[var(--btn-bg)] shadow-[0_0_0_1px_var(--btn-bg)] group-active:shadow-[0_0_0_0px_var(--btn-bg)]',
130
+ tertiary:
131
+ 'bg-active shadow-[0_0_0_1px_var(--border),inset_0_0_0_0px_var(--border)] group-active:shadow-[0_0_0_0px_var(--border),inset_0_0_0_1px_var(--border)]',
132
+ ghost: 'bg-active shadow-[0_0_0_1px_var(--active)] group-active:shadow-[0_0_0_0px_var(--active)]',
133
+ destructive:
134
+ 'bg-destructive-wash-active [--btn-bg:var(--destructive-wash-active)] shadow-[0_0_0_1px_var(--btn-bg)] group-active:shadow-[0_0_0_0px_var(--btn-bg)]',
135
+ success:
136
+ 'bg-success-wash-active [--btn-bg:var(--success-wash-active)] shadow-[0_0_0_1px_var(--btn-bg)] group-active:shadow-[0_0_0_0px_var(--btn-bg)]',
137
+ // A held link shows its underline.
138
+ link: 'bg-transparent',
139
+ }
140
+
141
+ const { exit: _exit, ...enter } = spring.moderate
142
+
143
+ /* Loading morph (local): the surface layer contracts from the button's box
144
+ into a circle one control tall around the spinner, on one progress value
145
+ like the morph layer (lib/use-morph.ts). The button's own box never
146
+ changes, so nothing around it moves. Left and right mix a share of the
147
+ button's width with pixels, so a stretched (w-full) button keeps the
148
+ circle centred without measuring its width. At rest the inline geometry
149
+ is dropped and the classes (inset-px, the inherited radius) take over. */
150
+ function useLoadingMorph(loading: boolean) {
151
+ const surface = useRef<HTMLSpanElement>(null)
152
+ const progress = useMotionValue(loading ? 1 : 0)
153
+ const reduced = useReduceMotion()
154
+
155
+ useLayoutEffect(() => {
156
+ const el = surface.current
157
+ const root = el?.parentElement
158
+ if (!el || !root || (!loading && progress.get() === 0)) return
159
+ const height = root.offsetHeight
160
+ // The surface sits 1px inside the button (its spread fills that pixel back out).
161
+ const circle = height / 2 - 1
162
+ const rest = Math.min(Number.parseFloat(getComputedStyle(root).borderTopLeftRadius) || 0, circle)
163
+ const render = (p: number) => {
164
+ const side = `calc(${50 * p}% + ${1 - (height / 2) * p}px)`
165
+ el.style.left = side
166
+ el.style.right = side
167
+ el.style.borderRadius = `${rest + (circle - rest) * p}px`
168
+ }
169
+ const settle = () => el.removeAttribute('style')
170
+ const target = loading ? 1 : 0
171
+ if (reduced || progress.get() === target) {
172
+ progress.jump(target)
173
+ return loading ? render(1) : settle()
174
+ }
175
+ const controls = animate(progress, target, {
176
+ ...enter,
177
+ onUpdate: render,
178
+ onComplete: loading ? undefined : settle,
179
+ })
180
+ return () => controls.stop()
181
+ }, [loading, reduced])
182
+
183
+ return surface
184
+ }
185
+
186
+ /**
187
+ * Versatile button with variants, sizes, loading state, and icon support.
188
+ *
189
+ * Four variants (primary, secondary, tertiary, ghost) on the two-step size
190
+ * ladder: 36px by default, 28px compact — an explicit `size` wins, otherwise
191
+ * the button follows the surrounding SizeProvider. For hosts with a
192
+ * shadcn-style API there are four more: `outline` (tertiary by its other
193
+ * name), `destructive` and `success` (the hue as the label on a wash of
194
+ * itself, never a solid fill, so a view keeps one primary) and `link`
195
+ * (underlined text on the control height). The press effect shrinks
196
+ * the fill by exactly 1px per side at any width, icons thicken their stroke
197
+ * on hover, and `loading` folds the surface into a circle around a spinner
198
+ * (and back when it ends) while the button keeps its box, so nothing around
199
+ * it moves. Built on Base UI's Button; `asChild` renders your own
200
+ * element (e.g. a link) with the button's styling instead.
201
+ *
202
+ * Also exported from this item:
203
+ * - `buttonVariants` — the class recipe (also from `button/variants`, safe in
204
+ * server code): `variant`, `size` (aliases included), `iconLeft`,
205
+ * `iconRight`, `surface`. On its own it gives the root classes, and the fill
206
+ * lives on the Button's inner layer; `surface: true` puts the fill, ring,
207
+ * hover, press and `aria-disabled` look on the element itself, for an
208
+ * anchor you can't render through `asChild`. Prefer `<Button asChild>`
209
+ * where you can: it keeps the press shrink, the icon stroke and `loading`.
210
+ *
211
+ * @example {@include ./examples.mdx}
212
+ */
213
+ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
214
+ (
215
+ {
216
+ className,
217
+ variant,
218
+ size,
219
+ asChild = false,
220
+ loading = false,
221
+ leadingIcon: LeadingIcon,
222
+ trailingIcon: TrailingIcon,
223
+ active = false,
224
+ disabled,
225
+ children,
226
+ style,
227
+ ...props
228
+ },
229
+ ref,
230
+ ) => {
231
+ // asChild: the user's element becomes the root while the button's internal
232
+ // structure (bg layer, content wrapper, spinner, icons) survives as its
233
+ // children — the element's own children become the label. We clone the
234
+ // element directly instead of routing through ButtonPrimitive's `render`:
235
+ // Base UI would bolt button semantics (role="button", Space activation)
236
+ // onto e.g. a link, where plain-link output is wanted.
237
+ const asChildElement =
238
+ asChild && isValidElement(children)
239
+ ? (children as ReactElement<{
240
+ children?: ReactNode
241
+ className?: string
242
+ style?: React.CSSProperties
243
+ ref?: React.Ref<HTMLButtonElement>
244
+ 'aria-busy'?: boolean
245
+ 'aria-disabled'?: boolean | 'true' | 'false'
246
+ onClick?: React.MouseEventHandler<HTMLButtonElement>
247
+ }>)
248
+ : null
249
+ const label = asChildElement ? asChildElement.props.children : children
250
+ // Resolve the size: explicit prop (legacy aliases mapped onto the
251
+ // canonical ladder) > surrounding SizeProvider > default.
252
+ const contextSize = useSizeVariant()
253
+ const resolvedSize: ButtonSizeCanonical = size
254
+ ? (legacySizeAliases[size] ?? (size as ButtonSizeCanonical))
255
+ : contextSize === 'compact'
256
+ ? 'compact'
257
+ : 'default'
258
+ const isIconOnly = resolvedSize === 'icon' || resolvedSize === 'icon-compact'
259
+ const isCompact = resolvedSize === 'compact' || resolvedSize === 'icon-compact'
260
+ const iconSize = isCompact ? 14 : 16
261
+ // Spinner box tracks the button height so the loading glyph stays
262
+ // proportionate across sizes.
263
+ const spinnerSizeClass = isCompact ? 'h-7 w-7' : 'h-9 w-9'
264
+ const shape = useShape()
265
+ // `outline` is tertiary by its shadcn name.
266
+ const surfaceVariant = variant === 'outline' ? 'tertiary' : (variant ?? 'primary')
267
+ const bgClass = active ? activeBgVariants[surfaceVariant] : bgVariants[surfaceVariant]
268
+
269
+ const surface = useLoadingMorph(loading)
270
+
271
+ const internals = (
272
+ <>
273
+ <span
274
+ ref={surface}
275
+ aria-hidden
276
+ className={cn(
277
+ 'absolute inset-px rounded-[inherit] transition-[box-shadow,background-color] [transition-duration:180ms,80ms] [transition-timing-function:cubic-bezier(0.23,1,0.32,1),ease] group-active:[transition-duration:80ms,80ms]',
278
+ bgClass,
279
+ )}
280
+ />
281
+ {/* The label keeps its place while loading, so the button keeps its
282
+ box. Whatever leaves goes on the fast exit, so the label and the
283
+ spinner barely overlap while the surface folds or unfolds. */}
284
+ <motion.span
285
+ className="relative inline-flex items-center justify-center gap-[inherit]"
286
+ initial={false}
287
+ animate={{ opacity: loading ? 0 : 1 }}
288
+ transition={loading ? spring.fast.exit : enter}
289
+ >
290
+ {isIconOnly ? (
291
+ <span className="[&_svg]:stroke-[1.5] [&_svg]:transition-[stroke-width] [&_svg]:duration-fast group-hover:[&_svg]:stroke-[2]">
292
+ {label}
293
+ </span>
294
+ ) : (
295
+ <>
296
+ {LeadingIcon && (
297
+ <LeadingIcon
298
+ size={iconSize}
299
+ strokeWidth={1.5}
300
+ className="transition-[stroke-width] duration-fast group-hover:stroke-[2]"
301
+ />
302
+ )}
303
+ {/* text-box only applies to block containers, so the trim lives
304
+ on the label span (a blockified flex item), not the flex root.
305
+ The button's height is fixed (h-*), so this doesn't change
306
+ layout — it just centers the cap-to-baseline box optically. */}
307
+ <span className="[text-box:trim-both_cap_alphabetic]">{label}</span>
308
+ {TrailingIcon && (
309
+ <TrailingIcon
310
+ size={iconSize}
311
+ strokeWidth={1.5}
312
+ className="transition-[stroke-width] duration-fast group-hover:stroke-[2]"
313
+ />
314
+ )}
315
+ </>
316
+ )}
317
+ </motion.span>
318
+ <AnimatePresence initial={false}>
319
+ {loading && (
320
+ <motion.span
321
+ key="spinner"
322
+ aria-hidden
323
+ className="absolute inset-0 flex items-center justify-center"
324
+ initial={{ opacity: 0, scale: 0.6 }}
325
+ animate={{ opacity: 1, scale: 1 }}
326
+ exit={{ opacity: 0, scale: 0.6, transition: spring.fast.exit }}
327
+ transition={enter}
328
+ >
329
+ <svg className={spinnerSizeClass} viewBox="0 0 24 24" fill="none">
330
+ <path
331
+ d="M 12 12 C 14 8.5 19 8.5 19 12 C 19 15.5 14 15.5 12 12 C 10 8.5 5 8.5 5 12 C 5 15.5 10 15.5 12 12 Z"
332
+ stroke="currentColor"
333
+ strokeWidth="1.125"
334
+ strokeLinecap="round"
335
+ pathLength="100"
336
+ style={{
337
+ strokeDasharray: '15 85',
338
+ animation: 'spinner-move 2s linear infinite, spinner-dash 4s ease-in-out infinite',
339
+ }}
340
+ />
341
+ </svg>
342
+ </motion.span>
343
+ )}
344
+ </AnimatePresence>
345
+ </>
346
+ )
347
+
348
+ const rootClassName = cn(
349
+ buttonVariants({
350
+ variant,
351
+ size: resolvedSize,
352
+ iconLeft: !isIconOnly && !!LeadingIcon,
353
+ iconRight: !isIconOnly && !!TrailingIcon,
354
+ }),
355
+ shape.button,
356
+ // A loading button is disabled through aria-disabled (it keeps focus),
357
+ // which the variants' `disabled:` styles don't see.
358
+ 'aria-disabled:opacity-50 aria-disabled:pointer-events-none',
359
+ active && variant === 'link' && 'decoration-current',
360
+ className,
361
+ )
362
+
363
+ if (asChildElement) {
364
+ const childProps = asChildElement.props
365
+ const onClick = props.onClick ?? childProps.onClick
366
+ return cloneElement(
367
+ asChildElement,
368
+ {
369
+ ...props,
370
+ ref,
371
+ 'aria-busy': loading || undefined,
372
+ 'aria-disabled': loading || props['aria-disabled'] || childProps['aria-disabled'],
373
+ // A pending action blocks a second press (and a link's navigation).
374
+ onClick: (event: React.MouseEvent<HTMLButtonElement>) => {
375
+ if (loading) {
376
+ event.preventDefault()
377
+ event.stopPropagation()
378
+ return
379
+ }
380
+ onClick?.(event)
381
+ },
382
+ className: cn(rootClassName, childProps.className),
383
+ style: { ...style, ...childProps.style },
384
+ },
385
+ internals,
386
+ )
387
+ }
388
+
389
+ return (
390
+ <ButtonPrimitive
391
+ // Base UI's `ButtonPrimitive` forwards to an HTMLButtonElement;
392
+ // keep the public ref type narrow so consumers see the right type.
393
+ ref={ref as React.Ref<HTMLButtonElement>}
394
+ className={rootClassName}
395
+ disabled={disabled || loading}
396
+ // While loading, Base UI disables it with aria-disabled instead of
397
+ // the attribute, so a keyboard user keeps focus on it; it still
398
+ // ignores presses.
399
+ focusableWhenDisabled={loading}
400
+ aria-busy={loading || undefined}
401
+ style={style}
402
+ {...props}
403
+ >
404
+ {internals}
405
+ </ButtonPrimitive>
406
+ )
407
+ },
408
+ )
409
+
410
+ Button.displayName = 'Button'
411
+
412
+ export type { ButtonSizeCanonical, ButtonVariantsOptions } from './variants'
413
+ export type { ButtonProps, ButtonSize, ButtonVariant }
414
+ export { Button, buttonVariants }
415
+
416
+ export default Button
@@ -0,0 +1,25 @@
1
+ /*
2
+ * Local addition (not part of Fluid Functionalism): the docs stage of the
3
+ * Button page's loading morph. A Button that loads for a moment when pressed,
4
+ * so the example stays hook-free.
5
+ */
6
+
7
+ import { useEffect, useState } from 'react'
8
+ import Button, { type ButtonProps } from './index'
9
+
10
+ interface LoadingDemoProps extends ButtonProps {
11
+ /** How long a press keeps the button loading, in ms. */
12
+ duration?: number
13
+ }
14
+
15
+ export default function LoadingDemo({ duration = 2000, ...props }: LoadingDemoProps) {
16
+ const [loading, setLoading] = useState(false)
17
+
18
+ useEffect(() => {
19
+ if (!loading) return
20
+ const timer = setTimeout(() => setLoading(false), duration)
21
+ return () => clearTimeout(timer)
22
+ }, [loading, duration])
23
+
24
+ return <Button {...props} loading={loading} onClick={() => setLoading(true)} />
25
+ }