@justanarthur/modo-ui 0.6.0-canary.8 → 0.6.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 (490) hide show
  1. package/README.md +66 -164
  2. package/dist/base.css +4 -141
  3. package/dist/blocks/sidebar-app/index.js +26 -40
  4. package/dist/components/AI/input-message/index.js +16 -23
  5. package/dist/components/AI/thinking-indicator/index.js +6 -9
  6. package/dist/components/accordion/index.d.ts +11 -39
  7. package/dist/components/accordion/index.js +13 -18
  8. package/dist/components/badge/index.d.ts +11 -18
  9. package/dist/components/badge/index.js +7 -12
  10. package/dist/components/button/index.d.ts +17 -53
  11. package/dist/components/button/index.js +6 -11
  12. package/dist/components/card/index.d.ts +9 -32
  13. package/dist/components/card/index.js +11 -17
  14. package/dist/components/copy/index.d.ts +8 -41
  15. package/dist/components/copy/index.js +9 -15
  16. package/dist/components/inputs/checkbox/index.d.ts +9 -28
  17. package/dist/components/inputs/checkbox/index.js +13 -18
  18. package/dist/components/inputs/color-picker/index.d.ts +14 -22
  19. package/dist/components/inputs/color-picker/index.js +18 -26
  20. package/dist/components/inputs/combobox/index.d.ts +20 -85
  21. package/dist/components/inputs/combobox/index.js +18 -27
  22. package/dist/components/inputs/date-time/calendar/index.d.ts +18 -24
  23. package/dist/components/inputs/date-time/calendar/index.js +11 -15
  24. package/dist/components/inputs/date-time/date-picker/index.d.ts +16 -105
  25. package/dist/components/inputs/date-time/date-picker/index.js +25 -35
  26. package/dist/components/inputs/date-time/date-range-picker/index.d.ts +12 -106
  27. package/dist/components/inputs/date-time/date-range-picker/index.js +24 -35
  28. package/dist/components/inputs/date-time/duration-picker/index.d.ts +13 -66
  29. package/dist/components/inputs/date-time/duration-picker/index.js +24 -33
  30. package/dist/components/inputs/date-time/month-picker/index.d.ts +10 -60
  31. package/dist/components/inputs/date-time/month-picker/index.js +23 -32
  32. package/dist/components/inputs/date-time/time-picker/index.d.ts +10 -58
  33. package/dist/components/inputs/date-time/time-picker/index.js +24 -33
  34. package/dist/components/inputs/input/index.d.ts +13 -95
  35. package/dist/components/inputs/input/index.js +9 -18
  36. package/dist/components/inputs/radio/index.d.ts +5 -20
  37. package/dist/components/inputs/radio/index.js +12 -16
  38. package/dist/components/inputs/search/index.d.ts +3 -3
  39. package/dist/components/inputs/search/index.js +18 -26
  40. package/dist/components/inputs/select/index.d.ts +23 -99
  41. package/dist/components/inputs/select/index.js +14 -29
  42. package/dist/components/inputs/slider/index.d.ts +0 -9
  43. package/dist/components/inputs/slider/index.js +5 -8
  44. package/dist/components/inputs/switch/index.d.ts +2 -8
  45. package/dist/components/inputs/switch/index.js +12 -15
  46. package/dist/components/inputs/textarea/index.d.ts +1 -20
  47. package/dist/components/inputs/textarea/index.js +6 -9
  48. package/dist/components/navigation/sidebar/index.d.ts +21 -44
  49. package/dist/components/navigation/sidebar/index.js +31 -42
  50. package/dist/components/navigation/tabs/index.d.ts +2 -8
  51. package/dist/components/navigation/tabs/index.js +10 -15
  52. package/dist/components/navigation/tabs-subtle/index.js +10 -13
  53. package/dist/components/overlays/alert-dialog/index.d.ts +16 -135
  54. package/dist/components/overlays/alert-dialog/index.js +13 -23
  55. package/dist/components/overlays/command/index.d.ts +20 -25
  56. package/dist/components/overlays/command/index.js +17 -25
  57. package/dist/components/overlays/context-menu/index.d.ts +17 -39
  58. package/dist/components/overlays/context-menu/index.js +18 -29
  59. package/dist/components/overlays/dialog/index.d.ts +16 -42
  60. package/dist/components/overlays/dialog/index.js +12 -20
  61. package/dist/components/overlays/dropdown/index.d.ts +34 -304
  62. package/dist/components/overlays/dropdown/index.js +17 -44
  63. package/dist/components/overlays/popover/index.d.ts +12 -38
  64. package/dist/components/overlays/popover/index.js +9 -13
  65. package/dist/components/overlays/preview-card/index.js +8 -12
  66. package/dist/components/overlays/sheet/index.d.ts +8 -25
  67. package/dist/components/overlays/sheet/index.js +13 -19
  68. package/dist/components/overlays/toast/index.d.ts +50 -209
  69. package/dist/components/overlays/toast/index.js +10 -23
  70. package/dist/components/overlays/tooltip/index.d.ts +21 -123
  71. package/dist/components/overlays/tooltip/index.js +6 -16
  72. package/dist/components/table/index.d.ts +8 -63
  73. package/dist/components/table/index.js +9 -20
  74. package/dist/index.d.ts +1048 -2999
  75. package/dist/index.js +82 -231
  76. package/dist/preset.d.ts +0 -22
  77. package/dist/preset.js +2 -24
  78. package/dist/primitives/code/index.js +5 -8
  79. package/dist/primitives/craft/index.js +6 -10
  80. package/dist/primitives/fluid-hover/index.js +8 -10
  81. package/dist/primitives/morph/index.js +10 -16
  82. package/dist/primitives/motion/index.js +8 -11
  83. package/dist/primitives/scroll-area/index.js +3 -5
  84. package/dist/primitives/sizes/index.js +4 -6
  85. package/dist/primitives/surface/index.js +4 -6
  86. package/dist/shared/{components-4y816hwp.js → components-0q0sqfh4.js} +49 -27
  87. package/dist/shared/components-0sn7ypjw.js +61 -0
  88. package/dist/shared/{components-vc864c51.js → components-11da9ajg.js} +5 -8
  89. package/dist/shared/components-1434p1mh.js +123 -0
  90. package/dist/shared/{components-t2a3qmy9.js → components-1z6tdadk.js} +24 -40
  91. package/dist/shared/{components-qs4rkqsj.js → components-27nnvrwe.js} +14 -15
  92. package/dist/shared/{components-av5t26rc.js → components-2dxkv6zm.js} +7 -8
  93. package/dist/shared/{components-wqrvqq5p.js → components-2ecg8svx.js} +152 -234
  94. package/dist/shared/{components-3ydr2fqe.js → components-2ph6t61s.js} +34 -37
  95. package/dist/shared/{components-6gk118w3.js → components-39jq9va1.js} +24 -29
  96. package/dist/shared/components-3ky44908.js +9 -0
  97. package/dist/shared/{components-y1qvbawa.js → components-3mkepka5.js} +25 -28
  98. package/dist/shared/{components-xqszvkj4.js → components-4700p9kz.js} +22 -24
  99. package/dist/shared/{components-yhgq0qxx.js → components-59efxjd9.js} +12 -13
  100. package/dist/shared/{components-9j9abkct.js → components-5vgpnxgy.js} +27 -51
  101. package/dist/shared/{components-8tt3xbb5.js → components-5xc2j8jz.js} +18 -19
  102. package/dist/shared/{components-jymmaevv.js → components-6p8tfcd6.js} +29 -57
  103. package/dist/shared/{components-4nck8acq.js → components-7r3etddy.js} +54 -19
  104. package/dist/shared/{components-skfd7kff.js → components-8snj490h.js} +32 -78
  105. package/dist/shared/{components-ctqmge8t.js → components-b0eac83j.js} +30 -9
  106. package/dist/shared/{components-9sttq78t.js → components-b2wyq2cq.js} +59 -41
  107. package/dist/shared/{components-vkwq2k96.js → components-b3c5et5b.js} +38 -49
  108. package/dist/shared/{components-rrbpmfx4.js → components-c6s6b43g.js} +2 -3
  109. package/dist/shared/{components-pw06rbxq.js → components-chacnk61.js} +19 -19
  110. package/dist/shared/{components-71nbt03m.js → components-cv16mryv.js} +7 -26
  111. package/dist/shared/{components-eqze3y30.js → components-d6xtjht3.js} +30 -35
  112. package/dist/shared/{components-5y5shqfd.js → components-dk0gjr5a.js} +18 -19
  113. package/dist/shared/{components-khspp26b.js → components-fzrq6tr3.js} +34 -37
  114. package/dist/shared/{components-km7fhgbk.js → components-gc5vtq2f.js} +30 -66
  115. package/dist/shared/components-gf5s6ap9.js +42 -0
  116. package/dist/shared/{components-qqcmtrbv.js → components-gz8zf3ec.js} +55 -56
  117. package/dist/shared/{components-mattt5jp.js → components-h2ajmhwn.js} +35 -107
  118. package/dist/shared/{components-awra5sff.js → components-hqeq3w9q.js} +16 -24
  119. package/dist/shared/{components-5tzhqbcw.js → components-hw9yk32z.js} +24 -36
  120. package/dist/shared/components-hyq23phv.js +310 -0
  121. package/dist/shared/{components-8mt5bsv6.js → components-jbbmdr74.js} +22 -41
  122. package/dist/shared/{components-dcc02hs2.js → components-jjw69c4c.js} +4 -7
  123. package/dist/shared/{components-2syq4mwc.js → components-jthe1pwb.js} +420 -139
  124. package/dist/shared/{components-r7q1zm7r.js → components-jwst3vtj.js} +76 -224
  125. package/dist/shared/{components-qdm3yh26.js → components-kvr7f9hy.js} +10 -19
  126. package/dist/shared/{components-s8yqk2dc.js → components-kwa6mrf9.js} +11 -46
  127. package/dist/shared/{components-de99ykt7.js → components-men63rwj.js} +2 -3
  128. package/dist/shared/{components-6b0zb8z3.js → components-mjthm1kf.js} +9 -13
  129. package/dist/shared/{components-2c64r8c9.js → components-n42c79bz.js} +22 -23
  130. package/dist/shared/{components-tz1fv35x.js → components-n7mjr357.js} +2 -3
  131. package/dist/shared/components-ng72k97e.js +128 -0
  132. package/dist/shared/{components-dyae0y7h.js → components-pbmqctfa.js} +65 -169
  133. package/dist/shared/{components-px2zm7dc.js → components-q5xj5c16.js} +8 -9
  134. package/dist/shared/components-raknhppb.js +64 -0
  135. package/dist/shared/{components-8rb25gbe.js → components-rr7a6ja9.js} +27 -59
  136. package/dist/shared/{components-5012r6gr.js → components-s083f38z.js} +34 -164
  137. package/dist/shared/components-s5ngg32c.js +267 -0
  138. package/dist/shared/components-sn1rxx1d.js +143 -0
  139. package/dist/shared/{components-r8eqs7gz.js → components-ta7shw28.js} +4 -4
  140. package/dist/shared/{components-9as3rf68.js → components-vc7vq5nz.js} +3 -6
  141. package/dist/shared/{components-ft3zdc79.js → components-vfbhxf5r.js} +5 -6
  142. package/dist/shared/components-vynjtrk6.js +19 -0
  143. package/dist/shared/{components-w0xq6q9k.js → components-w1r67ag9.js} +149 -294
  144. package/dist/shared/{components-z0bqtvcp.js → components-w90endve.js} +72 -71
  145. package/dist/shared/{components-t5pk5nmc.js → components-wvp1m52g.js} +102 -149
  146. package/dist/shared/components-wy9jpqny.js +146 -0
  147. package/dist/shared/{components-5sxjeh6h.js → components-y9y0g8er.js} +28 -52
  148. package/dist/shared/components-ypp01w3p.js +986 -0
  149. package/dist/shared/{components-t50hrswb.js → components-zfnmn5d4.js} +36 -82
  150. package/dist/shared/{components-jqpefjsn.js → components-zrkzcayz.js} +6 -54
  151. package/dist/shared/{components-vf45j12c.js → components-zx6b6yby.js} +18 -4
  152. package/dist/styles.css +32 -320
  153. package/dist/tailwind.css +0 -16
  154. package/dist/tokens.css +0 -67
  155. package/package.json +4 -227
  156. package/DESIGN.md +0 -623
  157. package/blocks/sidebar-app/examples.mdx +0 -33
  158. package/blocks/sidebar-app/index.tsx +0 -218
  159. package/blocks/sidebar-app/inset-topbar.tsx +0 -25
  160. package/blocks/sidebar-app/menu-grid.ts +0 -26
  161. package/blocks/sidebar-app/nav-data.ts +0 -36
  162. package/blocks/sidebar-app/search-field.tsx +0 -48
  163. package/blocks/sidebar-app/user-footer.tsx +0 -67
  164. package/blocks/sidebar-app/workspace-header.tsx +0 -129
  165. package/components/AI/input-message/examples.mdx +0 -130
  166. package/components/AI/input-message/file-thumbnail.tsx +0 -173
  167. package/components/AI/input-message/index.tsx +0 -1385
  168. package/components/AI/thinking-indicator/examples.mdx +0 -27
  169. package/components/AI/thinking-indicator/index.tsx +0 -210
  170. package/components/accordion/examples.mdx +0 -233
  171. package/components/accordion/index.tsx +0 -1071
  172. package/components/avatar/examples.mdx +0 -95
  173. package/components/avatar/index.tsx +0 -129
  174. package/components/badge/change-demo.tsx +0 -29
  175. package/components/badge/examples.mdx +0 -124
  176. package/components/badge/index.tsx +0 -225
  177. package/components/badge/variants.ts +0 -73
  178. package/components/button/examples.mdx +0 -168
  179. package/components/button/index.tsx +0 -424
  180. package/components/button/loading-demo.tsx +0 -25
  181. package/components/button/variants.ts +0 -168
  182. package/components/card/examples.mdx +0 -251
  183. package/components/card/index.tsx +0 -1070
  184. package/components/copy/examples.mdx +0 -125
  185. package/components/copy/index.tsx +0 -662
  186. package/components/inputs/checkbox/examples.mdx +0 -87
  187. package/components/inputs/checkbox/index.tsx +0 -611
  188. package/components/inputs/checkbox/select-all-demo.tsx +0 -48
  189. package/components/inputs/color-picker/examples.mdx +0 -111
  190. package/components/inputs/color-picker/index.tsx +0 -1981
  191. package/components/inputs/color-picker/variants.ts +0 -401
  192. package/components/inputs/combobox/examples.mdx +0 -366
  193. package/components/inputs/combobox/index.tsx +0 -1527
  194. package/components/inputs/combobox/remote-demo.tsx +0 -105
  195. package/components/inputs/date-time/calendar/examples.mdx +0 -45
  196. package/components/inputs/date-time/calendar/index.tsx +0 -370
  197. package/components/inputs/date-time/calendar/use-month-window.ts +0 -173
  198. package/components/inputs/date-time/date-picker/examples.mdx +0 -243
  199. package/components/inputs/date-time/date-picker/index.tsx +0 -476
  200. package/components/inputs/date-time/date-picker/masked-date-input.tsx +0 -348
  201. package/components/inputs/date-time/date-range-picker/examples.mdx +0 -93
  202. package/components/inputs/date-time/date-range-picker/index.tsx +0 -525
  203. package/components/inputs/date-time/duration-picker/examples.mdx +0 -40
  204. package/components/inputs/date-time/duration-picker/index.tsx +0 -253
  205. package/components/inputs/date-time/month-picker/examples.mdx +0 -45
  206. package/components/inputs/date-time/month-picker/index.tsx +0 -254
  207. package/components/inputs/date-time/picker-shell.tsx +0 -581
  208. package/components/inputs/date-time/time-columns.tsx +0 -94
  209. package/components/inputs/date-time/time-picker/examples.mdx +0 -57
  210. package/components/inputs/date-time/time-picker/index.tsx +0 -227
  211. package/components/inputs/field/examples.mdx +0 -117
  212. package/components/inputs/field/index.tsx +0 -268
  213. package/components/inputs/file-dropzone/examples.mdx +0 -83
  214. package/components/inputs/file-dropzone/file-row.tsx +0 -201
  215. package/components/inputs/file-dropzone/index.tsx +0 -316
  216. package/components/inputs/file-dropzone/upload-demo.tsx +0 -67
  217. package/components/inputs/file-dropzone/variants.ts +0 -94
  218. package/components/inputs/input/examples.mdx +0 -166
  219. package/components/inputs/input/index.tsx +0 -494
  220. package/components/inputs/input/password-demo.tsx +0 -30
  221. package/components/inputs/label/examples.mdx +0 -67
  222. package/components/inputs/label/index.tsx +0 -85
  223. package/components/inputs/number-input/examples.mdx +0 -69
  224. package/components/inputs/number-input/index.tsx +0 -221
  225. package/components/inputs/radio/examples.mdx +0 -77
  226. package/components/inputs/radio/index.tsx +0 -636
  227. package/components/inputs/search/examples.mdx +0 -74
  228. package/components/inputs/search/index.tsx +0 -333
  229. package/components/inputs/select/examples.mdx +0 -296
  230. package/components/inputs/select/index.tsx +0 -1044
  231. package/components/inputs/select/variants.ts +0 -32
  232. package/components/inputs/slider/examples.mdx +0 -153
  233. package/components/inputs/slider/index.tsx +0 -1867
  234. package/components/inputs/switch/examples.mdx +0 -96
  235. package/components/inputs/switch/index.tsx +0 -504
  236. package/components/inputs/textarea/examples.mdx +0 -126
  237. package/components/inputs/textarea/index.tsx +0 -207
  238. package/components/navigation/navigation-menu/examples.mdx +0 -136
  239. package/components/navigation/navigation-menu/index.tsx +0 -704
  240. package/components/navigation/pagination/examples.mdx +0 -74
  241. package/components/navigation/pagination/index.tsx +0 -401
  242. package/components/navigation/sidebar/examples.mdx +0 -325
  243. package/components/navigation/sidebar/index.tsx +0 -453
  244. package/components/navigation/sidebar/sidebar-core.tsx +0 -1408
  245. package/components/navigation/sidebar/sidebar-menu.tsx +0 -1495
  246. package/components/navigation/sidebar/sidebar.css +0 -74
  247. package/components/navigation/sidebar/variants.ts +0 -55
  248. package/components/navigation/tabs/examples.mdx +0 -89
  249. package/components/navigation/tabs/index.tsx +0 -494
  250. package/components/navigation/tabs-subtle/examples.mdx +0 -49
  251. package/components/navigation/tabs-subtle/index.tsx +0 -512
  252. package/components/overlays/alert-dialog/examples.mdx +0 -123
  253. package/components/overlays/alert-dialog/index.tsx +0 -276
  254. package/components/overlays/alert-dialog/pending-demo.tsx +0 -36
  255. package/components/overlays/command/examples.mdx +0 -113
  256. package/components/overlays/command/index.tsx +0 -425
  257. package/components/overlays/context-menu/examples.mdx +0 -56
  258. package/components/overlays/context-menu/index.tsx +0 -279
  259. package/components/overlays/dialog/examples.mdx +0 -165
  260. package/components/overlays/dialog/index.tsx +0 -451
  261. package/components/overlays/dropdown/dropdown-search.tsx +0 -395
  262. package/components/overlays/dropdown/examples.mdx +0 -444
  263. package/components/overlays/dropdown/index.tsx +0 -1241
  264. package/components/overlays/dropdown/menu-item.tsx +0 -510
  265. package/components/overlays/dropdown/menu-parts.tsx +0 -725
  266. package/components/overlays/popover/anchored-demo.tsx +0 -59
  267. package/components/overlays/popover/examples.mdx +0 -203
  268. package/components/overlays/popover/index.tsx +0 -317
  269. package/components/overlays/preview-card/examples.mdx +0 -61
  270. package/components/overlays/preview-card/index.tsx +0 -204
  271. package/components/overlays/sheet/examples.mdx +0 -101
  272. package/components/overlays/sheet/index.tsx +0 -307
  273. package/components/overlays/toast/examples.mdx +0 -85
  274. package/components/overlays/toast/global-demo.tsx +0 -72
  275. package/components/overlays/toast/index.tsx +0 -544
  276. package/components/overlays/toast/manager.ts +0 -193
  277. package/components/overlays/tooltip/examples.mdx +0 -106
  278. package/components/overlays/tooltip/index.tsx +0 -539
  279. package/components/progress/examples.mdx +0 -59
  280. package/components/progress/index.tsx +0 -174
  281. package/components/progress/progress.css +0 -49
  282. package/components/progress/step-demo.tsx +0 -27
  283. package/components/table/examples.mdx +0 -188
  284. package/components/table/index.tsx +0 -535
  285. package/components/table/sort-demo.tsx +0 -61
  286. package/dist/components/avatar/index.d.ts +0 -46
  287. package/dist/components/avatar/index.js +0 -19
  288. package/dist/components/badge/variants.d.ts +0 -33
  289. package/dist/components/badge/variants.js +0 -10
  290. package/dist/components/button/variants.d.ts +0 -39
  291. package/dist/components/button/variants.js +0 -8
  292. package/dist/components/inputs/color-picker/variants.d.ts +0 -61
  293. package/dist/components/inputs/color-picker/variants.js +0 -28
  294. package/dist/components/inputs/field/index.d.ts +0 -116
  295. package/dist/components/inputs/field/index.js +0 -29
  296. package/dist/components/inputs/file-dropzone/index.d.ts +0 -167
  297. package/dist/components/inputs/file-dropzone/index.js +0 -33
  298. package/dist/components/inputs/file-dropzone/variants.d.ts +0 -35
  299. package/dist/components/inputs/file-dropzone/variants.js +0 -12
  300. package/dist/components/inputs/label/index.d.ts +0 -32
  301. package/dist/components/inputs/label/index.js +0 -11
  302. package/dist/components/inputs/number-input/index.d.ts +0 -87
  303. package/dist/components/inputs/number-input/index.js +0 -21
  304. package/dist/components/inputs/select/variants.d.ts +0 -5
  305. package/dist/components/inputs/select/variants.js +0 -6
  306. package/dist/components/navigation/navigation-menu/index.d.ts +0 -126
  307. package/dist/components/navigation/navigation-menu/index.js +0 -41
  308. package/dist/components/navigation/pagination/index.d.ts +0 -75
  309. package/dist/components/navigation/pagination/index.js +0 -41
  310. package/dist/components/navigation/sidebar/variants.d.ts +0 -25
  311. package/dist/components/navigation/sidebar/variants.js +0 -28
  312. package/dist/components/progress/index.d.ts +0 -56
  313. package/dist/components/progress/index.js +0 -15
  314. package/dist/docs/index.d.ts +0 -83
  315. package/dist/docs/index.js +0 -288
  316. package/dist/docs/vite.d.ts +0 -31
  317. package/dist/docs/vite.js +0 -45
  318. package/dist/lib/date-locale.d.ts +0 -26
  319. package/dist/lib/date-locale.js +0 -24
  320. package/dist/lib/field-classes.d.ts +0 -149
  321. package/dist/lib/field-classes.js +0 -41
  322. package/dist/lib/field-variants.d.ts +0 -47
  323. package/dist/lib/field-variants.js +0 -26
  324. package/dist/lib/fluid-hover-highlight.d.ts +0 -112
  325. package/dist/lib/fluid-hover-highlight.js +0 -17
  326. package/dist/lib/focus-ring.d.ts +0 -18
  327. package/dist/lib/focus-ring.js +0 -11
  328. package/dist/lib/goo-indicator.d.ts +0 -46
  329. package/dist/lib/goo-indicator.js +0 -14
  330. package/dist/lib/icon-context.d.ts +0 -30
  331. package/dist/lib/icon-context.js +0 -12
  332. package/dist/lib/inert.d.ts +0 -9
  333. package/dist/lib/inert.js +0 -8
  334. package/dist/lib/morph-layers.d.ts +0 -93
  335. package/dist/lib/morph-layers.js +0 -13
  336. package/dist/lib/morph-part.d.ts +0 -21
  337. package/dist/lib/morph-part.js +0 -12
  338. package/dist/lib/popup.d.ts +0 -20
  339. package/dist/lib/popup.js +0 -12
  340. package/dist/lib/reduced-motion.d.ts +0 -9
  341. package/dist/lib/reduced-motion.js +0 -6
  342. package/dist/lib/row-index.d.ts +0 -10
  343. package/dist/lib/row-index.js +0 -8
  344. package/dist/lib/row-order.d.ts +0 -28
  345. package/dist/lib/row-order.js +0 -10
  346. package/dist/lib/scroll-column.d.ts +0 -31
  347. package/dist/lib/scroll-column.js +0 -19
  348. package/dist/lib/shape-context.d.ts +0 -30
  349. package/dist/lib/shape-context.js +0 -13
  350. package/dist/lib/shape-radius.d.ts +0 -19
  351. package/dist/lib/shape-radius.js +0 -6
  352. package/dist/lib/size-context.d.ts +0 -94
  353. package/dist/lib/size-context.js +0 -20
  354. package/dist/lib/size-scale.d.ts +0 -79
  355. package/dist/lib/size-scale.js +0 -8
  356. package/dist/lib/slot.d.ts +0 -39
  357. package/dist/lib/slot.js +0 -14
  358. package/dist/lib/springs.d.ts +0 -44
  359. package/dist/lib/springs.js +0 -8
  360. package/dist/lib/surface-classes.d.ts +0 -19
  361. package/dist/lib/surface-classes.js +0 -10
  362. package/dist/lib/surface-context.d.ts +0 -9
  363. package/dist/lib/surface-context.js +0 -8
  364. package/dist/lib/use-controllable-state.d.ts +0 -2
  365. package/dist/lib/use-controllable-state.js +0 -6
  366. package/dist/lib/use-fluid-hover.d.ts +0 -138
  367. package/dist/lib/use-fluid-hover.js +0 -14
  368. package/dist/lib/use-is-mobile.d.ts +0 -2
  369. package/dist/lib/use-is-mobile.js +0 -6
  370. package/dist/lib/use-keyboard-nav-gate.d.ts +0 -20
  371. package/dist/lib/use-keyboard-nav-gate.js +0 -7
  372. package/dist/lib/use-merge-split.d.ts +0 -47
  373. package/dist/lib/use-merge-split.js +0 -15
  374. package/dist/lib/use-morph.d.ts +0 -101
  375. package/dist/lib/use-morph.js +0 -20
  376. package/dist/lib/use-touch-primary.d.ts +0 -2
  377. package/dist/lib/use-touch-primary.js +0 -6
  378. package/dist/lib/utils.d.ts +0 -3
  379. package/dist/lib/utils.js +0 -6
  380. package/dist/primitives/separator/index.d.ts +0 -25
  381. package/dist/primitives/separator/index.js +0 -9
  382. package/dist/primitives/skeleton/index.d.ts +0 -35
  383. package/dist/primitives/skeleton/index.js +0 -13
  384. package/dist/shared/components-124epb7f.js +0 -53
  385. package/dist/shared/components-1kab3j44.js +0 -337
  386. package/dist/shared/components-26snnbs9.js +0 -322
  387. package/dist/shared/components-34txk9rj.js +0 -35
  388. package/dist/shared/components-3h92d94m.js +0 -23
  389. package/dist/shared/components-4vfm04wq.js +0 -82
  390. package/dist/shared/components-54nq988h.js +0 -123
  391. package/dist/shared/components-7s7pykk5.js +0 -281
  392. package/dist/shared/components-7zkhz3r8.js +0 -399
  393. package/dist/shared/components-8v644a0s.js +0 -298
  394. package/dist/shared/components-a4evvq8h.js +0 -271
  395. package/dist/shared/components-adkarj8n.js +0 -35
  396. package/dist/shared/components-b1e239g3.js +0 -28
  397. package/dist/shared/components-b94t6qtj.js +0 -143
  398. package/dist/shared/components-c9yr4ed1.js +0 -44
  399. package/dist/shared/components-cwg1d0w6.js +0 -254
  400. package/dist/shared/components-d3shbwqq.js +0 -95
  401. package/dist/shared/components-dhgk6mbt.js +0 -16
  402. package/dist/shared/components-drxrtnp2.js +0 -25
  403. package/dist/shared/components-ej4r7wmj.js +0 -482
  404. package/dist/shared/components-f7h57m6t.js +0 -59
  405. package/dist/shared/components-fejdzvqc.js +0 -448
  406. package/dist/shared/components-gx4h5rbb.js +0 -40
  407. package/dist/shared/components-gyt7my41.js +0 -328
  408. package/dist/shared/components-h6p86be3.js +0 -19
  409. package/dist/shared/components-h83az0bz.js +0 -33
  410. package/dist/shared/components-hdzrqqpx.js +0 -55
  411. package/dist/shared/components-krtx3m5b.js +0 -84
  412. package/dist/shared/components-kswxnm3h.js +0 -24
  413. package/dist/shared/components-mpa2y6x8.js +0 -277
  414. package/dist/shared/components-mrbaxt8q.js +0 -62
  415. package/dist/shared/components-pt5ftm4p.js +0 -38
  416. package/dist/shared/components-rpw4md2b.js +0 -179
  417. package/dist/shared/components-rtadhd9j.js +0 -64
  418. package/dist/shared/components-tz4gaf1v.js +0 -23
  419. package/dist/shared/components-wbbckbw8.js +0 -34
  420. package/dist/shared/components-xdqmt6ed.js +0 -20
  421. package/dist/shared/components-xjkdvnbg.js +0 -107
  422. package/dist/shared/components-z3v0n0de.js +0 -11
  423. package/dist/shared/components-z6dfqtyt.js +0 -1545
  424. package/dist/tokens/colors.css +0 -230
  425. package/dist/tokens/motion.css +0 -40
  426. package/dist/tokens/radius.css +0 -10
  427. package/dist/tokens/spacing.css +0 -9
  428. package/dist/tokens/typography.css +0 -119
  429. package/docs/chrome.css +0 -120
  430. package/docs/chrome.tsx +0 -283
  431. package/docs/index.ts +0 -9
  432. package/docs/vite.ts +0 -85
  433. package/index.ts +0 -60
  434. package/lib/date-locale.ts +0 -195
  435. package/lib/field-classes.ts +0 -121
  436. package/lib/field-variants.ts +0 -117
  437. package/lib/fluid-hover-highlight.tsx +0 -139
  438. package/lib/focus-ring.tsx +0 -46
  439. package/lib/goo-indicator.tsx +0 -261
  440. package/lib/icon-context.tsx +0 -310
  441. package/lib/inert.ts +0 -22
  442. package/lib/morph-layers.tsx +0 -102
  443. package/lib/morph-part.tsx +0 -49
  444. package/lib/popup.ts +0 -52
  445. package/lib/reduced-motion.ts +0 -19
  446. package/lib/row-index.tsx +0 -29
  447. package/lib/row-order.tsx +0 -126
  448. package/lib/scroll-column.tsx +0 -273
  449. package/lib/shape-context.tsx +0 -138
  450. package/lib/shape-radius.ts +0 -67
  451. package/lib/size-context.tsx +0 -66
  452. package/lib/size-scale.ts +0 -115
  453. package/lib/slot.ts +0 -115
  454. package/lib/springs.ts +0 -62
  455. package/lib/surface-classes.ts +0 -58
  456. package/lib/surface-context.tsx +0 -21
  457. package/lib/use-controllable-state.ts +0 -44
  458. package/lib/use-fluid-hover.ts +0 -541
  459. package/lib/use-is-mobile.ts +0 -25
  460. package/lib/use-keyboard-nav-gate.ts +0 -45
  461. package/lib/use-merge-split.tsx +0 -209
  462. package/lib/use-morph.ts +0 -557
  463. package/lib/use-touch-primary.tsx +0 -43
  464. package/lib/utils.ts +0 -37
  465. package/preset.ts +0 -199
  466. package/primitives/code/examples.mdx +0 -27
  467. package/primitives/code/index.tsx +0 -80
  468. package/primitives/craft/demos.tsx +0 -173
  469. package/primitives/craft/examples.mdx +0 -142
  470. package/primitives/craft/index.tsx +0 -76
  471. package/primitives/fluid-hover/examples.mdx +0 -84
  472. package/primitives/fluid-hover/index.tsx +0 -316
  473. package/primitives/morph/examples.mdx +0 -139
  474. package/primitives/morph/index.tsx +0 -192
  475. package/primitives/motion/examples.mdx +0 -100
  476. package/primitives/motion/index.tsx +0 -160
  477. package/primitives/scroll-area/examples.mdx +0 -58
  478. package/primitives/scroll-area/index.tsx +0 -204
  479. package/primitives/separator/examples.mdx +0 -30
  480. package/primitives/separator/index.tsx +0 -53
  481. package/primitives/sizes/examples.mdx +0 -167
  482. package/primitives/sizes/index.tsx +0 -89
  483. package/primitives/skeleton/examples.mdx +0 -51
  484. package/primitives/skeleton/index.tsx +0 -72
  485. package/primitives/skeleton/skeleton.css +0 -15
  486. package/primitives/surface/examples.mdx +0 -173
  487. package/primitives/surface/index.tsx +0 -109
  488. package/primitives/surface/surface.css +0 -176
  489. /package/dist/shared/{components-3ey0hzpb.js → components-1v7rxytx.js} +0 -0
  490. /package/dist/shared/{components-jcnjay5g.js → components-ezxhrmer.js} +0 -0
@@ -1,1070 +0,0 @@
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/default/card.tsx` @ b3587bdbd83fc66c2a6aae3817ffb856cb09260b
5
- * MIT License © 2026 Micka Touillaud — fluidfunctionalism.com (notice: LICENSE.fluid-functionalism)
6
- * Local modifications:
7
- * - `"use client"` dropped; `@/lib/*`, `@/hooks/*` and
8
- * `@/components/ui/fluid-hover-highlight` → `../../lib/*`, except
9
- * `SizeProvider` → `../../primitives/sizes`.
10
- * - `next/link` replaced with plain anchors (no Next.js in modo) — the
11
- * stretched card link and CardButton's `href` form.
12
- * - Prop JSDoc on CardProps rewritten from the FF docs API table (`children`
13
- * re-declared so it is documented); modo TSDoc and examples on `Card`.
14
- * - Compound statics `Card.Group/.Header/.Title/.Description/.Action/
15
- * .Content/.Footer/.Media/.Image/.Eyebrow/.Feature/.Button` attached with
16
- * `Object.assign`, typed through a `CardComponent` cast on the forwardRef.
17
- * Upstream's named exports are kept.
18
- * - Styling reads DS tokens (AGENTS.md styling): `text-[Npx]` →
19
- * `text-<role>[-compact]`; inline `fontVariationSettings` → `weight-*`; the
20
- * hex focus-ring fallback → `ring-focus-ring` / `border-focus-ring`;
21
- * `rounded-[Npx]` → radius tokens; `duration-80|120|160` and tier-length JS
22
- * durations → `duration-<tier>` / `spring.*`.
23
- * - `border` on a standalone Card (local): `"outlined"` draws the same hairline
24
- * frame an outlined Card.Group gives its tiles; inside a group the group's
25
- * `border` still decides. Borderless stays the default.
26
- * - `Card.Divider` / `CardDivider` (local): a hairline across the card between
27
- * its parts (full-bleed, spaced like the parts), vertical in an inline card;
28
- * the Separator primitive in the card's divider tone.
29
- * - `CardTitle` takes `as` (a heading level or another tag; a `span` by default)
30
- * and `CardContent` takes `bodyText` (the ladder's body size instead of the
31
- * inherited one), both local and opt-in.
32
- */
33
-
34
- import {
35
- Children,
36
- cloneElement,
37
- createContext,
38
- type ForwardRefExoticComponent,
39
- forwardRef,
40
- type HTMLAttributes,
41
- isValidElement,
42
- type MouseEventHandler,
43
- type ReactElement,
44
- type ReactNode,
45
- type Ref,
46
- type RefAttributes,
47
- useContext,
48
- useMemo,
49
- useRef,
50
- } from 'react'
51
- import { FluidHoverHighlight } from '../../lib/fluid-hover-highlight'
52
- import { type IconComponent, useIcon } from '../../lib/icon-context'
53
- import { useShape } from '../../lib/shape-context'
54
- import { type SizeVariant, useSize } from '../../lib/size-context'
55
- import { useFluidHover, useRegisterFluidHoverItem } from '../../lib/use-fluid-hover'
56
- import { cn } from '../../lib/utils'
57
- import Separator, { type SeparatorProps } from '../../primitives/separator'
58
- import { SizeProvider } from '../../primitives/sizes'
59
-
60
- // ---------------------------------------------------------------------------
61
- // Card is shadcn/ui's compositional card — the same parts and `data-slot`
62
- // contract (Card, CardHeader, CardTitle, CardDescription, CardAction,
63
- // CardContent, CardFooter) — with the Fluid Functionalism layer on top:
64
- // design tokens, a weight-animated title, and a sibling CardGroup that owns
65
- // layout (stacked list, inline rows, or grid) plus the magnetic fluid hover
66
- // highlight that previews where a click will land.
67
- //
68
- // Unlike stock shadcn, the surface is transparent and borderless by default:
69
- // cards inherit whatever substrate their parent provides (see the Surfaces
70
- // system) and lean on hairline dividers / the fluid hover highlight rather than
71
- // a drawn frame. A Card renders fine on its own; inside a CardGroup it
72
- // registers itself so the group's highlight can find it.
73
- // ---------------------------------------------------------------------------
74
-
75
- type CardOrientation = 'card' | 'inline'
76
- type CardBorder = 'none' | 'outlined'
77
-
78
- // ── Group context ────────────────────────────────────────
79
-
80
- interface CardGroupContextValue {
81
- registerItem: (index: number, element: HTMLElement | null) => void
82
- activeIndex: number | null
83
- /** Index of the persistently-selected card, or -1. Its neighbours drop the
84
- * hairline that would otherwise cut across the selection fill. */
85
- selectedIndex: number
86
- orientation: CardOrientation
87
- columns: number
88
- count: number
89
- /** Individual cards carry their own border/tile shape (separated grids). */
90
- separated: boolean
91
- /** Inner hairline dividers are drawn between adjacent cards. */
92
- divided: boolean
93
- outlined: boolean
94
- }
95
-
96
- const CardGroupContext = createContext<CardGroupContextValue | null>(null)
97
-
98
- // ── Per-card context ─────────────────────────────────────
99
- // Lets the compositional parts (title, header, footer…) adapt to the card
100
- // they're inside without threading props: the title reads `emphasized` to
101
- // animate its weight; parts read `orientation` to switch padding/flow.
102
-
103
- interface CardContextValue {
104
- emphasized: boolean
105
- orientation: CardOrientation
106
- clickable: boolean
107
- /** An inline card holding a full CardImage centres its text + actions in a
108
- * column beside the image, so the footer drops below the text (hugged and
109
- * vertically centred) instead of trailing to the right. */
110
- hasImage: boolean
111
- }
112
-
113
- const CardContext = createContext<CardContextValue>({
114
- emphasized: false,
115
- orientation: 'card',
116
- clickable: false,
117
- hasImage: false,
118
- })
119
-
120
- // ── CardGroup ────────────────────────────────────────────
121
-
122
- interface CardGroupProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onDrag'> {
123
- /** How each card lays its own content out.
124
- * "card" — stacked vertically (media/header on top). "inline" — a
125
- * horizontal row (leading media, trailing footer), like a Table row.
126
- * @default "card" */
127
- orientation?: CardOrientation
128
- /** Number of grid columns. >1 enables 2-D fluid hover across rows and columns.
129
- * @default 1 */
130
- columns?: number
131
- /** "none" — borderless (default), separated only by subtle dividers.
132
- * "outlined" — draws a border: one shared frame when grouped, or one per
133
- * card when `separated`. @default "none" */
134
- border?: CardBorder
135
- /** Split the group into individually-shaped cards with a gap between them
136
- * (a grid of tiles) instead of one continuous divided block. @default false */
137
- separated?: boolean
138
- /** Enable the magnetic fluid-hover highlight. @default true */
139
- fluidHover?: boolean
140
- }
141
-
142
- const CardGroup = forwardRef<HTMLDivElement, CardGroupProps>(
143
- (
144
- {
145
- orientation = 'card',
146
- columns = 1,
147
- border = 'none',
148
- separated = false,
149
- fluidHover = true,
150
- className,
151
- children,
152
- ...props
153
- },
154
- ref,
155
- ) => {
156
- const containerRef = useRef<HTMLDivElement>(null)
157
- const shape = useShape()
158
-
159
- // >1 column wraps into a grid, where nearest-item must be resolved in two
160
- // dimensions; a single column is a plain vertical list.
161
- const axis = columns > 1 ? 'xy' : 'y'
162
- // A card grid has generous whitespace, so a gap click only routes when it
163
- // lands within a hand's width of the highlighted card.
164
- const hover = useFluidHover(containerRef, { axis, gapClick: { maxDistance: 16 } })
165
- const { activeIndex, handlers, registerItem } = hover
166
-
167
- // Assign each valid child a stable fluid hover index so callers never thread
168
- // one through by hand (Table asks for it; here the group owns it).
169
- const childArray = Children.toArray(children).filter(isValidElement)
170
- const count = childArray.length
171
- const indexed = childArray.map((child, i) => cloneElement(child as ReactElement<{ index?: number }>, { index: i }))
172
- // Which card is selected — so its neighbours can drop the divider that
173
- // would otherwise slice through the selection fill.
174
- const selectedIndex = childArray.findIndex(child => (child.props as { selected?: boolean }).selected)
175
-
176
- const outlined = border === 'outlined'
177
- const divided = !separated
178
-
179
- const contextValue = useMemo<CardGroupContextValue>(
180
- () => ({
181
- registerItem,
182
- activeIndex,
183
- selectedIndex,
184
- orientation,
185
- columns,
186
- count,
187
- separated,
188
- divided,
189
- outlined,
190
- }),
191
- [registerItem, activeIndex, selectedIndex, orientation, columns, count, separated, divided, outlined],
192
- )
193
-
194
- return (
195
- <CardGroupContext.Provider value={contextValue}>
196
- <div
197
- ref={node => {
198
- ;(containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node
199
- if (typeof ref === 'function') ref(node)
200
- else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node
201
- }}
202
- {...props}
203
- data-slot="card-group"
204
- data-orientation={orientation}
205
- className={cn(
206
- 'relative grid',
207
- // A shared frame clips the highlight + dividers to its rounded
208
- // corners; separated tiles clip themselves.
209
- outlined && !separated && `border border-border/60 overflow-hidden ${shape.container}`,
210
- separated ? 'gap-2' : 'gap-0',
211
- className,
212
- )}
213
- style={{
214
- gridTemplateColumns: `repeat(${Math.max(1, columns)}, minmax(0, 1fr))`,
215
- }}
216
- onMouseEnter={fluidHover ? handlers.onMouseEnter : undefined}
217
- onMouseMove={fluidHover ? handlers.onMouseMove : undefined}
218
- onMouseLeave={fluidHover ? handlers.onMouseLeave : undefined}
219
- onClick={fluidHover ? handlers.onClick : undefined}
220
- >
221
- {/* Fluid hover highlight — a single magnetic layer that springs to the
222
- card nearest the cursor, previewing where a click will land. */}
223
- <FluidHoverHighlight hover={hover} hidden={!fluidHover} className={cn('z-0', shape.container)} />
224
-
225
- {indexed}
226
- </div>
227
- </CardGroupContext.Provider>
228
- )
229
- },
230
- )
231
-
232
- CardGroup.displayName = 'CardGroup'
233
-
234
- // ── Card ─────────────────────────────────────────────────
235
-
236
- interface CardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onClick'> {
237
- /** Compositional parts: Card.Image, Card.Media, Card.Header, Card.Content, Card.Footer. */
238
- children?: ReactNode
239
- /** Makes the whole card a clickable target (stretched button); fluid hover previews it. Receives the click event, or takes no argument. */
240
- onClick?: MouseEventHandler<HTMLElement>
241
- /** Makes the whole card a link (stretched anchor). */
242
- href?: string
243
- /** Opens href in a new tab. Defaults to `false`. */
244
- external?: boolean
245
- /** Accessible name for the stretched link/button when the whole card is clickable (the visible title isn't wired up automatically). */
246
- label?: string
247
- /** Persistent selected fill + title emphasis, on top of fluid hover. The group drops the hairline dividers around the selected card so the fill reads clean. Defaults to `false`. */
248
- selected?: boolean
249
- /** Dims and disables the card. Defaults to `false`. */
250
- disabled?: boolean
251
- /** Shows a dismiss (✕) button, revealed on hover or focus by default. Defaults to `false`. */
252
- dismissible?: boolean
253
- /** Keeps the dismiss control hidden until the card is hovered or holds focus. Pass false for an always-visible ✕. Defaults to `true`. */
254
- dismissOnHover?: boolean
255
- /** Called when the dismiss button is pressed. */
256
- onDismiss?: () => void
257
- /** Pins the card to one step of the size ladder — compact tightens type and padding. Omitted, it follows the surrounding SizeProvider. */
258
- size?: SizeVariant
259
- /** Frame of a standalone card: `"outlined"` draws a hairline around it, like an outlined group's tiles. Inside a Card.Group the group's `border` decides. Defaults to `"none"`. */
260
- border?: CardBorder
261
- /** Injected by Card.Group — do not set by hand. */
262
- index?: number
263
- }
264
-
265
- type CardComponent = ForwardRefExoticComponent<CardProps & RefAttributes<HTMLDivElement>> & {
266
- Group: typeof CardGroup
267
- Header: typeof CardHeader
268
- Title: typeof CardTitle
269
- Description: typeof CardDescription
270
- Action: typeof CardAction
271
- Content: typeof CardContent
272
- Footer: typeof CardFooter
273
- Divider: typeof CardDivider
274
- Media: typeof CardMedia
275
- Image: typeof CardImage
276
- Eyebrow: typeof CardEyebrow
277
- Feature: typeof CardFeature
278
- Button: typeof CardButton
279
- }
280
-
281
- /**
282
- * shadcn's compositional card, dressed in Fluid Functionalism. Stacked,
283
- * inline, or grid — borderless by default with fluid hover that previews
284
- * where a click will land.
285
- *
286
- * The same parts and data-slot contract as shadcn's Card, on a transparent,
287
- * borderless surface: a card inherits its parent's substrate and leans on
288
- * hairline dividers rather than a drawn frame (a standalone card opts into
289
- * one with `border="outlined"`). Card.Group owns the layout (a
290
- * stacked list, inline rows or a grid) and assigns every child its fluid
291
- * hover index; only a clickable card (`onClick` or `href`) joins the magnetic
292
- * highlight. A clickable card is one stretched button or link, while footer
293
- * actions and Card.Action stay independently clickable above it.
294
- *
295
- * Statics: Card.Group (orientation "card" | "inline", columns, border "none"
296
- * | "outlined", separated, fluidHover), Card.Header (title + description
297
- * grid, pins Card.Action top-right), Card.Title (weight-animates when the card
298
- * is selected; `as` renders it as a heading level), Card.Description,
299
- * Card.Action, Card.Content (`bodyText` sets the body size), Card.Footer (the
300
- * actions row; it wraps), Card.Divider (a hairline across the card between
301
- * its parts, vertical in an inline card), Card.Media (an icon tile, a logo or a [logoA, logoB] pair),
302
- * Card.Image (full-bleed banner, or a leading image when inline),
303
- * Card.Eyebrow (small uppercase label), Card.Feature (icon + title +
304
- * description row) and Card.Button (primary | secondary | ghost | link
305
- * action; an anchor when given `href`).
306
- *
307
- * @example {@include ./examples.mdx}
308
- */
309
- const Card = forwardRef<HTMLDivElement, CardProps>(
310
- (
311
- {
312
- onClick,
313
- href,
314
- external,
315
- label,
316
- selected = false,
317
- disabled = false,
318
- dismissible = false,
319
- dismissOnHover = true,
320
- onDismiss,
321
- size,
322
- border = 'none',
323
- index,
324
- className,
325
- children,
326
- ...props
327
- },
328
- ref,
329
- ) => {
330
- const internalRef = useRef<HTMLDivElement>(null)
331
- const shape = useShape()
332
- // Resolve against the explicit prop first so this card's own padding and
333
- // gaps agree with the SizeProvider it renders for its children.
334
- const sizeClasses = useSize(size)
335
- const compact = sizeClasses.variant === 'compact'
336
- const group = useContext(CardGroupContext)
337
- const XIcon = useIcon('x')
338
-
339
- const orientation = group?.orientation ?? 'card'
340
- const columns = group?.columns ?? 1
341
- const count = group?.count ?? 1
342
- const separated = group?.separated ?? true
343
- const divided = group?.divided ?? false
344
- const outlined = group?.outlined ?? false
345
- const activeIndex = group?.activeIndex ?? null
346
- const selectedIndex = group?.selectedIndex ?? -1
347
-
348
- // Depend on the stable registerItem callback, not the whole group context —
349
- // the context object's identity changes on every hover/selection frame,
350
- // which would otherwise re-register every card each frame.
351
- // Only a card that can be clicked joins the highlight: a highlight on an
352
- // informational card would promise a click that has nowhere to land.
353
- const registerItem = href || onClick ? group?.registerItem : undefined
354
- useRegisterFluidHoverItem(registerItem, index, internalRef)
355
-
356
- // Divider geometry: draw a hairline toward the neighbour below / to the
357
- // right, but drop it next to the active OR selected card so the highlight
358
- // and selection fill read clean (the same trick Table uses on row borders).
359
- const col = index !== undefined ? index % columns : 0
360
- const hasBelow = index !== undefined && index + columns < count
361
- const hasRight = index !== undefined && col < columns - 1 && index + 1 < count
362
- const self = index ?? -1
363
- const touchesBelow = (i: number) => i === self || i === self + columns
364
- const touchesRight = (i: number) => i === self || i === self + 1
365
- const showBottom = divided && hasBelow && !(touchesBelow(activeIndex ?? -1) || touchesBelow(selectedIndex))
366
- const showRight = divided && hasRight && !(touchesRight(activeIndex ?? -1) || touchesRight(selectedIndex))
367
-
368
- const isInline = orientation === 'inline'
369
- // An inline card with a full-bleed image reflows so its actions stack under
370
- // the text (see the body wrapper below + CardHeader/CardFooter). Match the
371
- // image child by identity OR displayName so detection and the split below
372
- // agree even when module identity drifts (e.g. HMR duplication).
373
- const isCardImage = (child: ReactNode) =>
374
- isValidElement(child) &&
375
- (child.type === CardImage || (child.type as { displayName?: string })?.displayName === 'CardImage')
376
- const hasImage = Children.toArray(children).some(isCardImage)
377
- const inlineImage = isInline && hasImage
378
- const clickable = !!href || !!onClick
379
- // Title weight follows the persistent selected state only — fluid hover
380
- // previews via the highlight fill, not by bolding the label.
381
- const emphasized = selected
382
-
383
- // A standalone card (no group) is its own tile — always rounded + clipped.
384
- // Inside a group, a separated tile carries its own rounding + clip only when
385
- // it draws a visible frame; a borderless separated tile has no surface to
386
- // hug, so it stays unclipped and its media reads as a plain square, and a
387
- // card in a continuous block leans on the shared group frame for both.
388
- const tileShape = !group
389
- ? cn(shape.container, 'overflow-hidden', border === 'outlined' && 'border border-border/60')
390
- : separated && outlined
391
- ? cn(shape.container, 'overflow-hidden border border-border/60')
392
- : ''
393
-
394
- // Stretched overlay makes the whole card the click target while keeping
395
- // action buttons (higher z) independently clickable — the accessible
396
- // alternative to nesting interactive elements inside a button/anchor. A
397
- // disabled card drops the overlay entirely so it can't be tabbed to or
398
- // activated by keyboard (pointer-events-none only blocks the mouse).
399
- const overlay =
400
- clickable && !disabled ? (
401
- href ? (
402
- <a
403
- href={href}
404
- onClick={onClick}
405
- target={external ? '_blank' : undefined}
406
- rel={external ? 'noopener noreferrer' : undefined}
407
- aria-label={label}
408
- className="absolute inset-0 z-20 outline-none focus-visible:ring-1 focus-visible:ring-focus-ring rounded-[inherit]"
409
- />
410
- ) : (
411
- <button
412
- type="button"
413
- onClick={onClick}
414
- aria-label={label}
415
- aria-pressed={selected || undefined}
416
- className="absolute inset-0 z-20 outline-none focus-visible:ring-1 focus-visible:ring-focus-ring rounded-[inherit]"
417
- />
418
- )
419
- ) : null
420
-
421
- const cardContext = useMemo<CardContextValue>(
422
- () => ({ emphasized, orientation, clickable, hasImage }),
423
- [emphasized, orientation, clickable, hasImage],
424
- )
425
-
426
- // Inline image cards wrap their non-image parts in a centred column so the
427
- // title, description, and actions hug together and sit vertically centred
428
- // against the image instead of stretching to its full height.
429
- let body: ReactNode = children
430
- if (inlineImage) {
431
- const parts = Children.toArray(children)
432
- const image = parts.find(isCardImage)
433
- const rest = parts.filter(part => part !== image)
434
- body = (
435
- <>
436
- {image}
437
- <div
438
- className={cn('flex min-w-0 flex-1 flex-col justify-center gap-2', compact ? 'py-2.5 pr-3' : 'py-3.5 pr-4')}
439
- >
440
- {rest}
441
- </div>
442
- </>
443
- )
444
- }
445
-
446
- const card = (
447
- <CardContext.Provider value={cardContext}>
448
- <div
449
- ref={node => {
450
- ;(internalRef as React.MutableRefObject<HTMLDivElement | null>).current = node
451
- if (typeof ref === 'function') ref(node)
452
- else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node
453
- }}
454
- data-slot="card"
455
- data-fluid-hover-index={index}
456
- data-selected={selected || undefined}
457
- data-orientation={orientation}
458
- aria-disabled={disabled || undefined}
459
- className={cn(
460
- 'group/card relative z-10 min-w-0 min-h-[60px]',
461
- inlineImage
462
- ? // Image on the left; the text + actions ride in a centred
463
- // column beside it (see the wrapper in the body below).
464
- cn('flex flex-row items-center', compact ? 'gap-2.5' : 'gap-3')
465
- : isInline
466
- ? cn('flex flex-row items-center', compact ? 'gap-2.5 pl-3' : 'gap-3 pl-4')
467
- : cn('flex flex-col', compact ? 'pb-3' : 'pb-4'),
468
- // Standalone (no group) cards can't lean on the group highlight, so
469
- // they carry their own hover tint when interactive.
470
- !group && clickable && !disabled && 'transition-colors duration-fast hover:bg-hover',
471
- // Inline rows are single-line, so the corner dismiss would sit on
472
- // the title's tail: the header yields right padding whenever the
473
- // control is present — only while it's revealed in the on-hover
474
- // case, so the resting text keeps the full row (the group-header
475
- // gutter-swap pattern; instant, not transitioned).
476
- dismissible &&
477
- isInline &&
478
- (dismissOnHover
479
- ? '[&:hover_[data-slot=card-header]]:pr-10 [&:focus-within_[data-slot=card-header]]:pr-10'
480
- : '[&_[data-slot=card-header]]:pr-10'),
481
- tileShape,
482
- disabled && 'opacity-50 pointer-events-none',
483
- className,
484
- )}
485
- {...props}
486
- >
487
- {/* Persistent selected fill + dividers sit behind the static content
488
- (-z-10); the stretched overlay (z-20) sits above content so the
489
- whole card is clickable, and actions/dismiss (z-30) rise above the
490
- overlay to stay independently interactive. */}
491
- {selected && (
492
- <span aria-hidden className={cn('absolute inset-0 -z-10 bg-active pointer-events-none', shape.container)} />
493
- )}
494
-
495
- {/* Dividers between borderless neighbours. Where both hairlines meet,
496
- the vertical one stops 1px short so the horizontal hairline owns
497
- the crossing pixel — two 60% lines would otherwise stack there and
498
- read brighter than the rest of the grid. */}
499
- {showBottom && (
500
- <span aria-hidden className="absolute inset-x-0 bottom-0 h-px bg-border/60 pointer-events-none -z-10" />
501
- )}
502
- {showRight && (
503
- <span
504
- aria-hidden
505
- className={cn(
506
- 'absolute top-0 right-0 w-px bg-border/60 pointer-events-none -z-10',
507
- showBottom ? 'bottom-px' : 'bottom-0',
508
- )}
509
- />
510
- )}
511
-
512
- {overlay}
513
-
514
- {body}
515
-
516
- {/* Dismiss control sits above the stretched overlay. */}
517
- {dismissible && (
518
- <button
519
- type="button"
520
- onClick={onDismiss}
521
- aria-label="Dismiss"
522
- className={cn(
523
- 'absolute right-2 top-2 z-30 flex h-7 w-7 items-center justify-center text-muted-foreground hover:text-foreground cursor-pointer outline-none transition-colors duration-fast focus-visible:ring-1 focus-visible:ring-focus-ring',
524
- // Over media the control needs its own ground, or the icon
525
- // reads against whatever the image happens to be. Elsewhere
526
- // the usual hover fill is enough. The chip takes the button
527
- // radius either way.
528
- hasImage ? 'bg-card/70 backdrop-blur-sm hover:bg-card' : 'hover:bg-hover',
529
- // Revealed on hover / focus. focus-visible covers the keyboard
530
- // path, since the control is the only way to dismiss.
531
- // pointer-events gates alongside opacity: an invisible
532
- // control must not swallow taps meant for the card (touch has
533
- // no hover to reveal it). Keyboard focus still reaches it —
534
- // pointer-events never blocks tabbing.
535
- dismissOnHover &&
536
- 'pointer-events-none opacity-0 transition-opacity duration-fast group-hover/card:pointer-events-auto group-hover/card:opacity-100 group-focus-within/card:pointer-events-auto group-focus-within/card:opacity-100 focus-visible:pointer-events-auto focus-visible:opacity-100',
537
- shape.button,
538
- )}
539
- >
540
- <XIcon size={compact ? 13 : 15} strokeWidth={1.5} />
541
- </button>
542
- )}
543
- </div>
544
- </CardContext.Provider>
545
- )
546
-
547
- // A size prop pins the card (and everything ladder-aware inside it) to one
548
- // ladder step; the parts read the context.
549
- return size ? <SizeProvider size={size}>{card}</SizeProvider> : card
550
- },
551
- ) as CardComponent
552
-
553
- Card.displayName = 'Card'
554
-
555
- // ── CardHeader ───────────────────────────────────────────
556
- // shadcn's header grid: title + description stack, with CardAction pinned to
557
- // the top-right column. In an inline card it becomes the flexible text column
558
- // between the leading media and trailing footer.
559
-
560
- const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => {
561
- const { orientation, hasImage } = useContext(CardContext)
562
- const inlineImage = orientation === 'inline' && hasImage
563
- const sizeClasses = useSize()
564
- const compact = sizeClasses.variant === 'compact'
565
- return (
566
- <div
567
- ref={ref}
568
- data-slot="card-header"
569
- className={cn(
570
- 'grid auto-rows-min items-start gap-1 has-data-[slot=card-action]:grid-cols-[1fr_auto]',
571
- inlineImage
572
- ? 'min-w-0'
573
- : orientation === 'inline'
574
- ? cn('min-w-0 flex-1', compact ? 'py-2.5' : 'py-3.5')
575
- : compact
576
- ? 'px-3 pt-3'
577
- : 'px-4 pt-4',
578
- className,
579
- )}
580
- {...props}
581
- />
582
- )
583
- })
584
-
585
- CardHeader.displayName = 'CardHeader'
586
-
587
- // ── CardTitle ────────────────────────────────────────────
588
-
589
- type CardTitleTag = 'span' | 'div' | 'p' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
590
-
591
- interface CardTitleProps extends HTMLAttributes<HTMLSpanElement> {
592
- /** The element the title renders as: a heading level (`'h2'`, `'h3'`, …) for a page outline, or another tag. The title keeps its look whatever the tag. Defaults to `'span'`. */
593
- as?: CardTitleTag
594
- }
595
-
596
- const CardTitle = forwardRef<HTMLSpanElement, CardTitleProps>(
597
- ({ as: Tag = 'span', className, children, ...props }, ref) => {
598
- const { emphasized, orientation } = useContext(CardContext)
599
- const sizeClasses = useSize()
600
- const compact = sizeClasses.variant === 'compact'
601
- // Inline rows trim the title to cap height so it centres tightly against
602
- // the media/actions; stacked cards keep the natural line box.
603
- const trim = orientation === 'inline' ? '[text-box:trim-both_cap_alphabetic]' : ''
604
- // Ghost-span pattern: an invisible semibold copy reserves the width so the
605
- // resting→active weight animation never reflows the row.
606
- //
607
- // Truncation support: a consumer's `truncate` lands on this grid, whose
608
- // text-overflow can't ellipsize (only a block's own line boxes can — grid
609
- // children just clip). The nowrap it sets inherits into the cell spans,
610
- // so giving those spans overflow-hidden + ellipsis renders the … there
611
- // instead; minmax(0,1fr) clamps the implicit track to the clipped grid's
612
- // width so the ellipsis falls inside the visible box. All of it is inert
613
- // while titles wrap normally.
614
- return (
615
- <Tag
616
- ref={ref as Ref<HTMLHeadingElement>}
617
- data-slot="card-title"
618
- className={cn(
619
- 'inline-grid grid-cols-[minmax(0,1fr)] leading-snug',
620
- Tag !== 'span' && 'm-0',
621
- compact ? 'text-subtitle-compact' : 'text-subtitle',
622
- className,
623
- )}
624
- {...props}
625
- >
626
- <span
627
- className={cn(
628
- 'col-start-1 row-start-1 invisible min-w-0 overflow-hidden text-ellipsis',
629
- trim,
630
- 'weight-semibold',
631
- )}
632
- aria-hidden="true"
633
- >
634
- {children}
635
- </span>
636
- <span
637
- className={cn(
638
- 'col-start-1 row-start-1 min-w-0 overflow-hidden text-ellipsis text-foreground transition-[font-variation-settings] duration-fast',
639
- trim,
640
- // normal → semibold on emphasis, matching nav-item / menu-item /
641
- // table (the opsz-paired tokens keep the advance width ~constant).
642
- emphasized ? 'weight-semibold' : 'weight-normal',
643
- )}
644
- >
645
- {children}
646
- </span>
647
- </Tag>
648
- )
649
- },
650
- )
651
-
652
- CardTitle.displayName = 'CardTitle'
653
-
654
- // ── CardDescription ──────────────────────────────────────
655
-
656
- const CardDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
657
- ({ className, ...props }, ref) => {
658
- const sizeClasses = useSize()
659
- const compact = sizeClasses.variant === 'compact'
660
- return (
661
- <p
662
- ref={ref}
663
- data-slot="card-description"
664
- className={cn(
665
- 'leading-normal text-muted-foreground',
666
- compact ? 'text-subtitle-compact' : 'text-subtitle',
667
- className,
668
- )}
669
- {...props}
670
- />
671
- )
672
- },
673
- )
674
-
675
- CardDescription.displayName = 'CardDescription'
676
-
677
- // ── CardAction ───────────────────────────────────────────
678
- // Pinned to the header's top-right column (shadcn's slot). Sits above the
679
- // stretched overlay so any control inside stays independently clickable.
680
-
681
- const CardAction = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
682
- <div
683
- ref={ref}
684
- data-slot="card-action"
685
- className={cn('relative z-30 col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}
686
- {...props}
687
- />
688
- ))
689
-
690
- CardAction.displayName = 'CardAction'
691
-
692
- // ── CardContent ──────────────────────────────────────────
693
-
694
- interface CardContentProps extends HTMLAttributes<HTMLDivElement> {
695
- /** Sets the size ladder's body text (`text-body`, one step down in a compact region) on the content, so plain text there no longer takes the size of whatever surrounds the card. Off, the content inherits it, as before. Defaults to `false`. */
696
- bodyText?: boolean
697
- }
698
-
699
- const CardContent = forwardRef<HTMLDivElement, CardContentProps>(({ bodyText = false, className, ...props }, ref) => {
700
- const { orientation } = useContext(CardContext)
701
- const sizeClasses = useSize()
702
- const compact = sizeClasses.variant === 'compact'
703
- return (
704
- <div
705
- ref={ref}
706
- data-slot="card-content"
707
- className={cn(
708
- orientation === 'inline' ? '' : compact ? 'px-3 pt-2.5' : 'px-4 pt-3',
709
- bodyText && sizeClasses.text,
710
- className,
711
- )}
712
- {...props}
713
- />
714
- )
715
- })
716
-
717
- CardContent.displayName = 'CardContent'
718
-
719
- // ── CardFooter ───────────────────────────────────────────
720
- // Actions row. Rises above the stretched overlay (z-30) so buttons stay
721
- // clickable. In an inline card it becomes the trailing, right-aligned slot.
722
-
723
- const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => {
724
- const { orientation, hasImage } = useContext(CardContext)
725
- const inlineImage = orientation === 'inline' && hasImage
726
- const sizeClasses = useSize()
727
- const compact = sizeClasses.variant === 'compact'
728
- return (
729
- <div
730
- ref={ref}
731
- data-slot="card-footer"
732
- className={cn(
733
- 'relative z-30 flex items-center gap-1',
734
- inlineImage
735
- ? // Under the text, left-aligned in natural order (the inline
736
- // wrapper owns the spacing).
737
- 'flex-wrap'
738
- : orientation === 'inline'
739
- ? cn('shrink-0 ml-auto', compact ? 'pr-3' : 'pr-4')
740
- : cn('flex-wrap', compact ? 'px-3 pt-2.5' : 'px-4 pt-3'),
741
- className,
742
- )}
743
- {...props}
744
- />
745
- )
746
- })
747
-
748
- CardFooter.displayName = 'CardFooter'
749
-
750
- // ── CardDivider ──────────────────────────────────────────
751
- // Local: a hairline across the card between two parts. The card has no
752
- // padding of its own, so a direct child runs edge to edge; its top margin
753
- // matches the parts' top padding, so the gap reads the same on both sides.
754
-
755
- const CardDivider = forwardRef<HTMLDivElement, SeparatorProps>(({ className, orientation, ...props }, ref) => {
756
- const { orientation: cardOrientation } = useContext(CardContext)
757
- const compact = useSize().variant === 'compact'
758
- const vertical = (orientation ?? (cardOrientation === 'inline' ? 'vertical' : 'horizontal')) === 'vertical'
759
- return (
760
- <Separator
761
- ref={ref}
762
- data-slot="card-divider"
763
- {...props}
764
- orientation={vertical ? 'vertical' : 'horizontal'}
765
- className={cn(
766
- 'bg-border/60',
767
- vertical ? (compact ? 'my-2.5' : 'my-3.5') : compact ? 'mt-2.5' : 'mt-3',
768
- className,
769
- )}
770
- />
771
- )
772
- })
773
-
774
- CardDivider.displayName = 'CardDivider'
775
-
776
- // ── CardMedia ────────────────────────────────────────────
777
- // FF addition: a leading icon or brand logo(s). Not part of shadcn's anatomy,
778
- // but the connective tissue most product cards need. A tuple renders a
779
- // connected logo pair (e.g. a trigger — target).
780
-
781
- type CardLogo = string | [string, string]
782
-
783
- interface CardMediaProps {
784
- logo?: CardLogo
785
- logoAlt?: string
786
- icon?: IconComponent
787
- size?: number
788
- className?: string
789
- }
790
-
791
- function CardMedia({ logo, logoAlt, icon: Icon, size = 22, className }: CardMediaProps) {
792
- const { orientation } = useContext(CardContext)
793
- const shape = useShape()
794
- const sizeClasses = useSize()
795
- const compact = sizeClasses.variant === 'compact'
796
- // Stacked: sits in the header grid; add an extra 8px so the gap below the
797
- // icon reads 12px (header gap-1 + mb-2). Inline: leading slot — the card owns
798
- // the left inset, so no extra padding here.
799
- const wrap = cn(orientation === 'inline' ? '' : 'mb-2', className)
800
-
801
- if (logo) {
802
- const logos = Array.isArray(logo) ? logo : [logo]
803
- return (
804
- <span data-slot="card-media" className={cn('inline-flex items-center gap-1.5 shrink-0', wrap)}>
805
- {logos.map((src, i) => (
806
- <span key={i} className="inline-flex items-center gap-1.5">
807
- {i > 0 && <span aria-hidden className="w-2 h-px bg-border" />}
808
- {/* eslint-disable-next-line @next/next/no-img-element */}
809
- <img
810
- src={src}
811
- alt={logoAlt ?? ''}
812
- width={size}
813
- height={size}
814
- className={cn('object-contain', shape.bg)}
815
- style={{ width: size, height: size }}
816
- />
817
- </span>
818
- ))}
819
- </span>
820
- )
821
- }
822
- if (Icon) {
823
- // The icon sits in a 32×32 tinted tile so it reads as a media slot rather
824
- // than a bare glyph. The tile is an overlay tint (not a solid surface) so it
825
- // blends over whatever is behind it — the substrate or the hover highlight.
826
- return (
827
- <span
828
- data-slot="card-media"
829
- className={cn('inline-flex items-center justify-center shrink-0 size-8 bg-hover', shape.bg, wrap)}
830
- >
831
- <Icon size={compact ? 16 : 18} strokeWidth={1.5} className="text-muted-foreground" />
832
- </span>
833
- )
834
- }
835
- return null
836
- }
837
-
838
- // ── CardImage ────────────────────────────────────────────
839
- // FF addition: the prominent, full-bleed image (distinct from a small logo).
840
- // Stacked → a top banner; inline → a full-height leading image on the left that
841
- // bleeds past the card's left inset to sit flush against the edge.
842
-
843
- interface CardImageProps {
844
- src: string
845
- alt?: string
846
- className?: string
847
- }
848
-
849
- function CardImage({ src, alt, className }: CardImageProps) {
850
- const { orientation } = useContext(CardContext)
851
- return (
852
- // eslint-disable-next-line @next/next/no-img-element
853
- <img
854
- src={src}
855
- alt={alt ?? ''}
856
- data-slot="card-image"
857
- // The image keeps a fixed 2px corner radius in every state — stacked or
858
- // inline, framed or borderless — rather than inheriting a frame's larger
859
- // clip. (A framed tile still clips the surrounding surface as before.)
860
- className={cn(
861
- 'object-cover rounded-xs',
862
- orientation === 'inline' ? 'size-40 shrink-0' : 'w-full aspect-[16/9]',
863
- className,
864
- )}
865
- />
866
- )
867
- }
868
-
869
- // A stable marker so the Card can recognise an image child by name, surviving
870
- // module-identity mismatches (e.g. HMR duplication) that break `type ===`.
871
- CardImage.displayName = 'CardImage'
872
-
873
- // ── CardEyebrow ──────────────────────────────────────────
874
- // Small uppercase label above the title (e.g. "New Model"). Typographically
875
- // it's the caption role of the type scale in uppercase — see /docs/sizes.
876
-
877
- const CardEyebrow = forwardRef<HTMLSpanElement, HTMLAttributes<HTMLSpanElement>>(({ className, ...props }, ref) => {
878
- const compact = useSize().variant === 'compact'
879
- return (
880
- <span
881
- ref={ref}
882
- data-slot="card-eyebrow"
883
- className={cn(
884
- compact ? 'text-caption-compact' : 'text-caption',
885
- 'uppercase tracking-wide text-muted-foreground',
886
- 'weight-semibold',
887
- className,
888
- )}
889
- {...props}
890
- />
891
- )
892
- })
893
-
894
- CardEyebrow.displayName = 'CardEyebrow'
895
-
896
- // ── CardFeature ──────────────────────────────────────────
897
- // FF addition: an icon + title + description row, for feature lists inside
898
- // CardContent.
899
-
900
- interface CardFeatureProps {
901
- icon?: IconComponent
902
- title: string
903
- description?: string
904
- }
905
-
906
- function CardFeature({ icon: Icon, title, description }: CardFeatureProps) {
907
- const sizeClasses = useSize()
908
- const compact = sizeClasses.variant === 'compact'
909
- return (
910
- <div data-slot="card-feature" className={cn('flex items-start', sizeClasses.gap)}>
911
- {Icon && <Icon size={sizeClasses.icon} strokeWidth={1.5} className="mt-0.5 shrink-0 text-muted-foreground" />}
912
- <div className="flex flex-col gap-0.5 min-w-0">
913
- <span className={cn('text-foreground [text-box:trim-both_cap_alphabetic]', sizeClasses.text, 'weight-medium')}>
914
- {title}
915
- </span>
916
- {description && (
917
- <span
918
- className={cn('leading-relaxed text-muted-foreground', compact ? 'text-caption-compact' : 'text-caption')}
919
- >
920
- {description}
921
- </span>
922
- )}
923
- </div>
924
- </div>
925
- )
926
- }
927
-
928
- // ── CardButton ───────────────────────────────────────────
929
- // Self-contained action button for the footer (keeps Card free of a Button
930
- // dependency, so it installs standalone). Renders an anchor when `href` is set.
931
-
932
- type CardButtonVariant = 'primary' | 'secondary' | 'ghost' | 'link'
933
-
934
- const CARD_BUTTON_VARIANTS: Record<CardButtonVariant, string> = {
935
- primary: 'bg-foreground text-background hover:bg-foreground/90 active:bg-foreground/80',
936
- secondary: 'bg-accent text-foreground hover:bg-accent/80 active:bg-accent',
937
- ghost: 'text-muted-foreground hover:text-foreground hover:bg-hover active:bg-active',
938
- link: 'text-foreground underline-offset-4 hover:underline !px-0 !h-auto',
939
- }
940
-
941
- interface CardButtonProps {
942
- children: React.ReactNode
943
- onClick?: () => void
944
- href?: string
945
- variant?: CardButtonVariant
946
- icon?: IconComponent
947
- iconPosition?: 'start' | 'end'
948
- /** Opens the href in a new tab and appends an outward arrow glyph. */
949
- external?: boolean
950
- disabled?: boolean
951
- }
952
-
953
- function CardButton({
954
- children,
955
- onClick,
956
- href,
957
- variant = 'ghost',
958
- icon: Icon,
959
- iconPosition,
960
- external = false,
961
- disabled = false,
962
- }: CardButtonProps) {
963
- const shape = useShape()
964
- const ArrowRight = useIcon('arrow-right')
965
- const sizeClasses = useSize()
966
- const compact = sizeClasses.variant === 'compact'
967
- const position = iconPosition ?? (external ? 'end' : 'start')
968
-
969
- const glyph = Icon ? (
970
- <Icon
971
- size={compact ? 12 : 14}
972
- strokeWidth={1.5}
973
- className="shrink-0 transition-[stroke-width] duration-fast group-hover/action:stroke-[2]"
974
- />
975
- ) : null
976
- const externalGlyph = external ? (
977
- <ArrowRight
978
- size={13}
979
- strokeWidth={1.5}
980
- className="shrink-0 -rotate-45 transition-[stroke-width] duration-fast group-hover/action:stroke-[2]"
981
- />
982
- ) : null
983
-
984
- const inner = (
985
- <>
986
- {position === 'start' && glyph}
987
- <span className="[text-box:trim-both_cap_alphabetic]">{children}</span>
988
- {position === 'end' && glyph}
989
- {externalGlyph}
990
- </>
991
- )
992
-
993
- const classes = cn(
994
- 'group/action relative z-30 inline-flex items-center justify-center gap-1.5 h-7 px-2.5 cursor-pointer outline-none',
995
- compact ? 'text-caption-compact' : 'text-caption',
996
- 'weight-medium',
997
- 'transition-colors duration-fast',
998
- 'focus-visible:ring-1 focus-visible:ring-focus-ring',
999
- 'disabled:opacity-50 disabled:pointer-events-none',
1000
- shape.button,
1001
- CARD_BUTTON_VARIANTS[variant],
1002
- )
1003
-
1004
- if (href) {
1005
- return (
1006
- <a
1007
- href={href}
1008
- onClick={onClick}
1009
- target={external ? '_blank' : undefined}
1010
- rel={external ? 'noopener noreferrer' : undefined}
1011
- className={classes}
1012
- >
1013
- {inner}
1014
- </a>
1015
- )
1016
- }
1017
-
1018
- return (
1019
- <button type="button" onClick={onClick} disabled={disabled} className={classes}>
1020
- {inner}
1021
- </button>
1022
- )
1023
- }
1024
-
1025
- // Compound statics: `<Card.Group>`, `<Card.Header>`, … (typed by the
1026
- // CardComponent cast on the forwardRef above).
1027
- Object.assign(Card, {
1028
- Group: CardGroup,
1029
- Header: CardHeader,
1030
- Title: CardTitle,
1031
- Description: CardDescription,
1032
- Action: CardAction,
1033
- Content: CardContent,
1034
- Footer: CardFooter,
1035
- Divider: CardDivider,
1036
- Media: CardMedia,
1037
- Image: CardImage,
1038
- Eyebrow: CardEyebrow,
1039
- Feature: CardFeature,
1040
- Button: CardButton,
1041
- })
1042
-
1043
- export type {
1044
- CardBorder,
1045
- CardButtonProps,
1046
- CardButtonVariant,
1047
- CardContentProps,
1048
- CardGroupProps,
1049
- CardLogo,
1050
- CardProps,
1051
- CardTitleProps,
1052
- CardTitleTag,
1053
- }
1054
- export {
1055
- Card,
1056
- CardAction,
1057
- CardButton,
1058
- CardContent,
1059
- CardDescription,
1060
- CardDivider,
1061
- CardEyebrow,
1062
- CardFeature,
1063
- CardFooter,
1064
- CardGroup,
1065
- CardHeader,
1066
- CardImage,
1067
- CardMedia,
1068
- CardTitle,
1069
- }
1070
- export default Card