@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.
- package/README.md +66 -164
- package/dist/base.css +4 -141
- package/dist/blocks/sidebar-app/index.js +26 -40
- package/dist/components/AI/input-message/index.js +16 -23
- package/dist/components/AI/thinking-indicator/index.js +6 -9
- package/dist/components/accordion/index.d.ts +11 -39
- package/dist/components/accordion/index.js +13 -18
- package/dist/components/badge/index.d.ts +11 -18
- package/dist/components/badge/index.js +7 -12
- package/dist/components/button/index.d.ts +17 -53
- package/dist/components/button/index.js +6 -11
- package/dist/components/card/index.d.ts +9 -32
- package/dist/components/card/index.js +11 -17
- package/dist/components/copy/index.d.ts +8 -41
- package/dist/components/copy/index.js +9 -15
- package/dist/components/inputs/checkbox/index.d.ts +9 -28
- package/dist/components/inputs/checkbox/index.js +13 -18
- package/dist/components/inputs/color-picker/index.d.ts +14 -22
- package/dist/components/inputs/color-picker/index.js +18 -26
- package/dist/components/inputs/combobox/index.d.ts +20 -85
- package/dist/components/inputs/combobox/index.js +18 -27
- package/dist/components/inputs/date-time/calendar/index.d.ts +18 -24
- package/dist/components/inputs/date-time/calendar/index.js +11 -15
- package/dist/components/inputs/date-time/date-picker/index.d.ts +16 -105
- package/dist/components/inputs/date-time/date-picker/index.js +25 -35
- package/dist/components/inputs/date-time/date-range-picker/index.d.ts +12 -106
- package/dist/components/inputs/date-time/date-range-picker/index.js +24 -35
- package/dist/components/inputs/date-time/duration-picker/index.d.ts +13 -66
- package/dist/components/inputs/date-time/duration-picker/index.js +24 -33
- package/dist/components/inputs/date-time/month-picker/index.d.ts +10 -60
- package/dist/components/inputs/date-time/month-picker/index.js +23 -32
- package/dist/components/inputs/date-time/time-picker/index.d.ts +10 -58
- package/dist/components/inputs/date-time/time-picker/index.js +24 -33
- package/dist/components/inputs/input/index.d.ts +13 -95
- package/dist/components/inputs/input/index.js +9 -18
- package/dist/components/inputs/radio/index.d.ts +5 -20
- package/dist/components/inputs/radio/index.js +12 -16
- package/dist/components/inputs/search/index.d.ts +3 -3
- package/dist/components/inputs/search/index.js +18 -26
- package/dist/components/inputs/select/index.d.ts +23 -99
- package/dist/components/inputs/select/index.js +14 -29
- package/dist/components/inputs/slider/index.d.ts +0 -9
- package/dist/components/inputs/slider/index.js +5 -8
- package/dist/components/inputs/switch/index.d.ts +2 -8
- package/dist/components/inputs/switch/index.js +12 -15
- package/dist/components/inputs/textarea/index.d.ts +1 -20
- package/dist/components/inputs/textarea/index.js +6 -9
- package/dist/components/navigation/sidebar/index.d.ts +21 -44
- package/dist/components/navigation/sidebar/index.js +31 -42
- package/dist/components/navigation/tabs/index.d.ts +2 -8
- package/dist/components/navigation/tabs/index.js +10 -15
- package/dist/components/navigation/tabs-subtle/index.js +10 -13
- package/dist/components/overlays/alert-dialog/index.d.ts +16 -135
- package/dist/components/overlays/alert-dialog/index.js +13 -23
- package/dist/components/overlays/command/index.d.ts +20 -25
- package/dist/components/overlays/command/index.js +17 -25
- package/dist/components/overlays/context-menu/index.d.ts +17 -39
- package/dist/components/overlays/context-menu/index.js +18 -29
- package/dist/components/overlays/dialog/index.d.ts +16 -42
- package/dist/components/overlays/dialog/index.js +12 -20
- package/dist/components/overlays/dropdown/index.d.ts +34 -304
- package/dist/components/overlays/dropdown/index.js +17 -44
- package/dist/components/overlays/popover/index.d.ts +12 -38
- package/dist/components/overlays/popover/index.js +9 -13
- package/dist/components/overlays/preview-card/index.js +8 -12
- package/dist/components/overlays/sheet/index.d.ts +8 -25
- package/dist/components/overlays/sheet/index.js +13 -19
- package/dist/components/overlays/toast/index.d.ts +50 -209
- package/dist/components/overlays/toast/index.js +10 -23
- package/dist/components/overlays/tooltip/index.d.ts +21 -123
- package/dist/components/overlays/tooltip/index.js +6 -16
- package/dist/components/table/index.d.ts +8 -63
- package/dist/components/table/index.js +9 -20
- package/dist/index.d.ts +1048 -2999
- package/dist/index.js +82 -231
- package/dist/preset.d.ts +0 -22
- package/dist/preset.js +2 -24
- package/dist/primitives/code/index.js +5 -8
- package/dist/primitives/craft/index.js +6 -10
- package/dist/primitives/fluid-hover/index.js +8 -10
- package/dist/primitives/morph/index.js +10 -16
- package/dist/primitives/motion/index.js +8 -11
- package/dist/primitives/scroll-area/index.js +3 -5
- package/dist/primitives/sizes/index.js +4 -6
- package/dist/primitives/surface/index.js +4 -6
- package/dist/shared/{components-4y816hwp.js → components-0q0sqfh4.js} +49 -27
- package/dist/shared/components-0sn7ypjw.js +61 -0
- package/dist/shared/{components-vc864c51.js → components-11da9ajg.js} +5 -8
- package/dist/shared/components-1434p1mh.js +123 -0
- package/dist/shared/{components-t2a3qmy9.js → components-1z6tdadk.js} +24 -40
- package/dist/shared/{components-qs4rkqsj.js → components-27nnvrwe.js} +14 -15
- package/dist/shared/{components-av5t26rc.js → components-2dxkv6zm.js} +7 -8
- package/dist/shared/{components-wqrvqq5p.js → components-2ecg8svx.js} +152 -234
- package/dist/shared/{components-3ydr2fqe.js → components-2ph6t61s.js} +34 -37
- package/dist/shared/{components-6gk118w3.js → components-39jq9va1.js} +24 -29
- package/dist/shared/components-3ky44908.js +9 -0
- package/dist/shared/{components-y1qvbawa.js → components-3mkepka5.js} +25 -28
- package/dist/shared/{components-xqszvkj4.js → components-4700p9kz.js} +22 -24
- package/dist/shared/{components-yhgq0qxx.js → components-59efxjd9.js} +12 -13
- package/dist/shared/{components-9j9abkct.js → components-5vgpnxgy.js} +27 -51
- package/dist/shared/{components-8tt3xbb5.js → components-5xc2j8jz.js} +18 -19
- package/dist/shared/{components-jymmaevv.js → components-6p8tfcd6.js} +29 -57
- package/dist/shared/{components-4nck8acq.js → components-7r3etddy.js} +54 -19
- package/dist/shared/{components-skfd7kff.js → components-8snj490h.js} +32 -78
- package/dist/shared/{components-ctqmge8t.js → components-b0eac83j.js} +30 -9
- package/dist/shared/{components-9sttq78t.js → components-b2wyq2cq.js} +59 -41
- package/dist/shared/{components-vkwq2k96.js → components-b3c5et5b.js} +38 -49
- package/dist/shared/{components-rrbpmfx4.js → components-c6s6b43g.js} +2 -3
- package/dist/shared/{components-pw06rbxq.js → components-chacnk61.js} +19 -19
- package/dist/shared/{components-71nbt03m.js → components-cv16mryv.js} +7 -26
- package/dist/shared/{components-eqze3y30.js → components-d6xtjht3.js} +30 -35
- package/dist/shared/{components-5y5shqfd.js → components-dk0gjr5a.js} +18 -19
- package/dist/shared/{components-khspp26b.js → components-fzrq6tr3.js} +34 -37
- package/dist/shared/{components-km7fhgbk.js → components-gc5vtq2f.js} +30 -66
- package/dist/shared/components-gf5s6ap9.js +42 -0
- package/dist/shared/{components-qqcmtrbv.js → components-gz8zf3ec.js} +55 -56
- package/dist/shared/{components-mattt5jp.js → components-h2ajmhwn.js} +35 -107
- package/dist/shared/{components-awra5sff.js → components-hqeq3w9q.js} +16 -24
- package/dist/shared/{components-5tzhqbcw.js → components-hw9yk32z.js} +24 -36
- package/dist/shared/components-hyq23phv.js +310 -0
- package/dist/shared/{components-8mt5bsv6.js → components-jbbmdr74.js} +22 -41
- package/dist/shared/{components-dcc02hs2.js → components-jjw69c4c.js} +4 -7
- package/dist/shared/{components-2syq4mwc.js → components-jthe1pwb.js} +420 -139
- package/dist/shared/{components-r7q1zm7r.js → components-jwst3vtj.js} +76 -224
- package/dist/shared/{components-qdm3yh26.js → components-kvr7f9hy.js} +10 -19
- package/dist/shared/{components-s8yqk2dc.js → components-kwa6mrf9.js} +11 -46
- package/dist/shared/{components-de99ykt7.js → components-men63rwj.js} +2 -3
- package/dist/shared/{components-6b0zb8z3.js → components-mjthm1kf.js} +9 -13
- package/dist/shared/{components-2c64r8c9.js → components-n42c79bz.js} +22 -23
- package/dist/shared/{components-tz1fv35x.js → components-n7mjr357.js} +2 -3
- package/dist/shared/components-ng72k97e.js +128 -0
- package/dist/shared/{components-dyae0y7h.js → components-pbmqctfa.js} +65 -169
- package/dist/shared/{components-px2zm7dc.js → components-q5xj5c16.js} +8 -9
- package/dist/shared/components-raknhppb.js +64 -0
- package/dist/shared/{components-8rb25gbe.js → components-rr7a6ja9.js} +27 -59
- package/dist/shared/{components-5012r6gr.js → components-s083f38z.js} +34 -164
- package/dist/shared/components-s5ngg32c.js +267 -0
- package/dist/shared/components-sn1rxx1d.js +143 -0
- package/dist/shared/{components-r8eqs7gz.js → components-ta7shw28.js} +4 -4
- package/dist/shared/{components-9as3rf68.js → components-vc7vq5nz.js} +3 -6
- package/dist/shared/{components-ft3zdc79.js → components-vfbhxf5r.js} +5 -6
- package/dist/shared/components-vynjtrk6.js +19 -0
- package/dist/shared/{components-w0xq6q9k.js → components-w1r67ag9.js} +149 -294
- package/dist/shared/{components-z0bqtvcp.js → components-w90endve.js} +72 -71
- package/dist/shared/{components-t5pk5nmc.js → components-wvp1m52g.js} +102 -149
- package/dist/shared/components-wy9jpqny.js +146 -0
- package/dist/shared/{components-5sxjeh6h.js → components-y9y0g8er.js} +28 -52
- package/dist/shared/components-ypp01w3p.js +986 -0
- package/dist/shared/{components-t50hrswb.js → components-zfnmn5d4.js} +36 -82
- package/dist/shared/{components-jqpefjsn.js → components-zrkzcayz.js} +6 -54
- package/dist/shared/{components-vf45j12c.js → components-zx6b6yby.js} +18 -4
- package/dist/styles.css +32 -320
- package/dist/tailwind.css +0 -16
- package/dist/tokens.css +0 -67
- package/package.json +4 -227
- package/DESIGN.md +0 -623
- package/blocks/sidebar-app/examples.mdx +0 -33
- package/blocks/sidebar-app/index.tsx +0 -218
- package/blocks/sidebar-app/inset-topbar.tsx +0 -25
- package/blocks/sidebar-app/menu-grid.ts +0 -26
- package/blocks/sidebar-app/nav-data.ts +0 -36
- package/blocks/sidebar-app/search-field.tsx +0 -48
- package/blocks/sidebar-app/user-footer.tsx +0 -67
- package/blocks/sidebar-app/workspace-header.tsx +0 -129
- package/components/AI/input-message/examples.mdx +0 -130
- package/components/AI/input-message/file-thumbnail.tsx +0 -173
- package/components/AI/input-message/index.tsx +0 -1385
- package/components/AI/thinking-indicator/examples.mdx +0 -27
- package/components/AI/thinking-indicator/index.tsx +0 -210
- package/components/accordion/examples.mdx +0 -233
- package/components/accordion/index.tsx +0 -1071
- package/components/avatar/examples.mdx +0 -95
- package/components/avatar/index.tsx +0 -129
- package/components/badge/change-demo.tsx +0 -29
- package/components/badge/examples.mdx +0 -124
- package/components/badge/index.tsx +0 -225
- package/components/badge/variants.ts +0 -73
- package/components/button/examples.mdx +0 -168
- package/components/button/index.tsx +0 -424
- package/components/button/loading-demo.tsx +0 -25
- package/components/button/variants.ts +0 -168
- package/components/card/examples.mdx +0 -251
- package/components/card/index.tsx +0 -1070
- package/components/copy/examples.mdx +0 -125
- package/components/copy/index.tsx +0 -662
- package/components/inputs/checkbox/examples.mdx +0 -87
- package/components/inputs/checkbox/index.tsx +0 -611
- package/components/inputs/checkbox/select-all-demo.tsx +0 -48
- package/components/inputs/color-picker/examples.mdx +0 -111
- package/components/inputs/color-picker/index.tsx +0 -1981
- package/components/inputs/color-picker/variants.ts +0 -401
- package/components/inputs/combobox/examples.mdx +0 -366
- package/components/inputs/combobox/index.tsx +0 -1527
- package/components/inputs/combobox/remote-demo.tsx +0 -105
- package/components/inputs/date-time/calendar/examples.mdx +0 -45
- package/components/inputs/date-time/calendar/index.tsx +0 -370
- package/components/inputs/date-time/calendar/use-month-window.ts +0 -173
- package/components/inputs/date-time/date-picker/examples.mdx +0 -243
- package/components/inputs/date-time/date-picker/index.tsx +0 -476
- package/components/inputs/date-time/date-picker/masked-date-input.tsx +0 -348
- package/components/inputs/date-time/date-range-picker/examples.mdx +0 -93
- package/components/inputs/date-time/date-range-picker/index.tsx +0 -525
- package/components/inputs/date-time/duration-picker/examples.mdx +0 -40
- package/components/inputs/date-time/duration-picker/index.tsx +0 -253
- package/components/inputs/date-time/month-picker/examples.mdx +0 -45
- package/components/inputs/date-time/month-picker/index.tsx +0 -254
- package/components/inputs/date-time/picker-shell.tsx +0 -581
- package/components/inputs/date-time/time-columns.tsx +0 -94
- package/components/inputs/date-time/time-picker/examples.mdx +0 -57
- package/components/inputs/date-time/time-picker/index.tsx +0 -227
- package/components/inputs/field/examples.mdx +0 -117
- package/components/inputs/field/index.tsx +0 -268
- package/components/inputs/file-dropzone/examples.mdx +0 -83
- package/components/inputs/file-dropzone/file-row.tsx +0 -201
- package/components/inputs/file-dropzone/index.tsx +0 -316
- package/components/inputs/file-dropzone/upload-demo.tsx +0 -67
- package/components/inputs/file-dropzone/variants.ts +0 -94
- package/components/inputs/input/examples.mdx +0 -166
- package/components/inputs/input/index.tsx +0 -494
- package/components/inputs/input/password-demo.tsx +0 -30
- package/components/inputs/label/examples.mdx +0 -67
- package/components/inputs/label/index.tsx +0 -85
- package/components/inputs/number-input/examples.mdx +0 -69
- package/components/inputs/number-input/index.tsx +0 -221
- package/components/inputs/radio/examples.mdx +0 -77
- package/components/inputs/radio/index.tsx +0 -636
- package/components/inputs/search/examples.mdx +0 -74
- package/components/inputs/search/index.tsx +0 -333
- package/components/inputs/select/examples.mdx +0 -296
- package/components/inputs/select/index.tsx +0 -1044
- package/components/inputs/select/variants.ts +0 -32
- package/components/inputs/slider/examples.mdx +0 -153
- package/components/inputs/slider/index.tsx +0 -1867
- package/components/inputs/switch/examples.mdx +0 -96
- package/components/inputs/switch/index.tsx +0 -504
- package/components/inputs/textarea/examples.mdx +0 -126
- package/components/inputs/textarea/index.tsx +0 -207
- package/components/navigation/navigation-menu/examples.mdx +0 -136
- package/components/navigation/navigation-menu/index.tsx +0 -704
- package/components/navigation/pagination/examples.mdx +0 -74
- package/components/navigation/pagination/index.tsx +0 -401
- package/components/navigation/sidebar/examples.mdx +0 -325
- package/components/navigation/sidebar/index.tsx +0 -453
- package/components/navigation/sidebar/sidebar-core.tsx +0 -1408
- package/components/navigation/sidebar/sidebar-menu.tsx +0 -1495
- package/components/navigation/sidebar/sidebar.css +0 -74
- package/components/navigation/sidebar/variants.ts +0 -55
- package/components/navigation/tabs/examples.mdx +0 -89
- package/components/navigation/tabs/index.tsx +0 -494
- package/components/navigation/tabs-subtle/examples.mdx +0 -49
- package/components/navigation/tabs-subtle/index.tsx +0 -512
- package/components/overlays/alert-dialog/examples.mdx +0 -123
- package/components/overlays/alert-dialog/index.tsx +0 -276
- package/components/overlays/alert-dialog/pending-demo.tsx +0 -36
- package/components/overlays/command/examples.mdx +0 -113
- package/components/overlays/command/index.tsx +0 -425
- package/components/overlays/context-menu/examples.mdx +0 -56
- package/components/overlays/context-menu/index.tsx +0 -279
- package/components/overlays/dialog/examples.mdx +0 -165
- package/components/overlays/dialog/index.tsx +0 -451
- package/components/overlays/dropdown/dropdown-search.tsx +0 -395
- package/components/overlays/dropdown/examples.mdx +0 -444
- package/components/overlays/dropdown/index.tsx +0 -1241
- package/components/overlays/dropdown/menu-item.tsx +0 -510
- package/components/overlays/dropdown/menu-parts.tsx +0 -725
- package/components/overlays/popover/anchored-demo.tsx +0 -59
- package/components/overlays/popover/examples.mdx +0 -203
- package/components/overlays/popover/index.tsx +0 -317
- package/components/overlays/preview-card/examples.mdx +0 -61
- package/components/overlays/preview-card/index.tsx +0 -204
- package/components/overlays/sheet/examples.mdx +0 -101
- package/components/overlays/sheet/index.tsx +0 -307
- package/components/overlays/toast/examples.mdx +0 -85
- package/components/overlays/toast/global-demo.tsx +0 -72
- package/components/overlays/toast/index.tsx +0 -544
- package/components/overlays/toast/manager.ts +0 -193
- package/components/overlays/tooltip/examples.mdx +0 -106
- package/components/overlays/tooltip/index.tsx +0 -539
- package/components/progress/examples.mdx +0 -59
- package/components/progress/index.tsx +0 -174
- package/components/progress/progress.css +0 -49
- package/components/progress/step-demo.tsx +0 -27
- package/components/table/examples.mdx +0 -188
- package/components/table/index.tsx +0 -535
- package/components/table/sort-demo.tsx +0 -61
- package/dist/components/avatar/index.d.ts +0 -46
- package/dist/components/avatar/index.js +0 -19
- package/dist/components/badge/variants.d.ts +0 -33
- package/dist/components/badge/variants.js +0 -10
- package/dist/components/button/variants.d.ts +0 -39
- package/dist/components/button/variants.js +0 -8
- package/dist/components/inputs/color-picker/variants.d.ts +0 -61
- package/dist/components/inputs/color-picker/variants.js +0 -28
- package/dist/components/inputs/field/index.d.ts +0 -116
- package/dist/components/inputs/field/index.js +0 -29
- package/dist/components/inputs/file-dropzone/index.d.ts +0 -167
- package/dist/components/inputs/file-dropzone/index.js +0 -33
- package/dist/components/inputs/file-dropzone/variants.d.ts +0 -35
- package/dist/components/inputs/file-dropzone/variants.js +0 -12
- package/dist/components/inputs/label/index.d.ts +0 -32
- package/dist/components/inputs/label/index.js +0 -11
- package/dist/components/inputs/number-input/index.d.ts +0 -87
- package/dist/components/inputs/number-input/index.js +0 -21
- package/dist/components/inputs/select/variants.d.ts +0 -5
- package/dist/components/inputs/select/variants.js +0 -6
- package/dist/components/navigation/navigation-menu/index.d.ts +0 -126
- package/dist/components/navigation/navigation-menu/index.js +0 -41
- package/dist/components/navigation/pagination/index.d.ts +0 -75
- package/dist/components/navigation/pagination/index.js +0 -41
- package/dist/components/navigation/sidebar/variants.d.ts +0 -25
- package/dist/components/navigation/sidebar/variants.js +0 -28
- package/dist/components/progress/index.d.ts +0 -56
- package/dist/components/progress/index.js +0 -15
- package/dist/docs/index.d.ts +0 -83
- package/dist/docs/index.js +0 -288
- package/dist/docs/vite.d.ts +0 -31
- package/dist/docs/vite.js +0 -45
- package/dist/lib/date-locale.d.ts +0 -26
- package/dist/lib/date-locale.js +0 -24
- package/dist/lib/field-classes.d.ts +0 -149
- package/dist/lib/field-classes.js +0 -41
- package/dist/lib/field-variants.d.ts +0 -47
- package/dist/lib/field-variants.js +0 -26
- package/dist/lib/fluid-hover-highlight.d.ts +0 -112
- package/dist/lib/fluid-hover-highlight.js +0 -17
- package/dist/lib/focus-ring.d.ts +0 -18
- package/dist/lib/focus-ring.js +0 -11
- package/dist/lib/goo-indicator.d.ts +0 -46
- package/dist/lib/goo-indicator.js +0 -14
- package/dist/lib/icon-context.d.ts +0 -30
- package/dist/lib/icon-context.js +0 -12
- package/dist/lib/inert.d.ts +0 -9
- package/dist/lib/inert.js +0 -8
- package/dist/lib/morph-layers.d.ts +0 -93
- package/dist/lib/morph-layers.js +0 -13
- package/dist/lib/morph-part.d.ts +0 -21
- package/dist/lib/morph-part.js +0 -12
- package/dist/lib/popup.d.ts +0 -20
- package/dist/lib/popup.js +0 -12
- package/dist/lib/reduced-motion.d.ts +0 -9
- package/dist/lib/reduced-motion.js +0 -6
- package/dist/lib/row-index.d.ts +0 -10
- package/dist/lib/row-index.js +0 -8
- package/dist/lib/row-order.d.ts +0 -28
- package/dist/lib/row-order.js +0 -10
- package/dist/lib/scroll-column.d.ts +0 -31
- package/dist/lib/scroll-column.js +0 -19
- package/dist/lib/shape-context.d.ts +0 -30
- package/dist/lib/shape-context.js +0 -13
- package/dist/lib/shape-radius.d.ts +0 -19
- package/dist/lib/shape-radius.js +0 -6
- package/dist/lib/size-context.d.ts +0 -94
- package/dist/lib/size-context.js +0 -20
- package/dist/lib/size-scale.d.ts +0 -79
- package/dist/lib/size-scale.js +0 -8
- package/dist/lib/slot.d.ts +0 -39
- package/dist/lib/slot.js +0 -14
- package/dist/lib/springs.d.ts +0 -44
- package/dist/lib/springs.js +0 -8
- package/dist/lib/surface-classes.d.ts +0 -19
- package/dist/lib/surface-classes.js +0 -10
- package/dist/lib/surface-context.d.ts +0 -9
- package/dist/lib/surface-context.js +0 -8
- package/dist/lib/use-controllable-state.d.ts +0 -2
- package/dist/lib/use-controllable-state.js +0 -6
- package/dist/lib/use-fluid-hover.d.ts +0 -138
- package/dist/lib/use-fluid-hover.js +0 -14
- package/dist/lib/use-is-mobile.d.ts +0 -2
- package/dist/lib/use-is-mobile.js +0 -6
- package/dist/lib/use-keyboard-nav-gate.d.ts +0 -20
- package/dist/lib/use-keyboard-nav-gate.js +0 -7
- package/dist/lib/use-merge-split.d.ts +0 -47
- package/dist/lib/use-merge-split.js +0 -15
- package/dist/lib/use-morph.d.ts +0 -101
- package/dist/lib/use-morph.js +0 -20
- package/dist/lib/use-touch-primary.d.ts +0 -2
- package/dist/lib/use-touch-primary.js +0 -6
- package/dist/lib/utils.d.ts +0 -3
- package/dist/lib/utils.js +0 -6
- package/dist/primitives/separator/index.d.ts +0 -25
- package/dist/primitives/separator/index.js +0 -9
- package/dist/primitives/skeleton/index.d.ts +0 -35
- package/dist/primitives/skeleton/index.js +0 -13
- package/dist/shared/components-124epb7f.js +0 -53
- package/dist/shared/components-1kab3j44.js +0 -337
- package/dist/shared/components-26snnbs9.js +0 -322
- package/dist/shared/components-34txk9rj.js +0 -35
- package/dist/shared/components-3h92d94m.js +0 -23
- package/dist/shared/components-4vfm04wq.js +0 -82
- package/dist/shared/components-54nq988h.js +0 -123
- package/dist/shared/components-7s7pykk5.js +0 -281
- package/dist/shared/components-7zkhz3r8.js +0 -399
- package/dist/shared/components-8v644a0s.js +0 -298
- package/dist/shared/components-a4evvq8h.js +0 -271
- package/dist/shared/components-adkarj8n.js +0 -35
- package/dist/shared/components-b1e239g3.js +0 -28
- package/dist/shared/components-b94t6qtj.js +0 -143
- package/dist/shared/components-c9yr4ed1.js +0 -44
- package/dist/shared/components-cwg1d0w6.js +0 -254
- package/dist/shared/components-d3shbwqq.js +0 -95
- package/dist/shared/components-dhgk6mbt.js +0 -16
- package/dist/shared/components-drxrtnp2.js +0 -25
- package/dist/shared/components-ej4r7wmj.js +0 -482
- package/dist/shared/components-f7h57m6t.js +0 -59
- package/dist/shared/components-fejdzvqc.js +0 -448
- package/dist/shared/components-gx4h5rbb.js +0 -40
- package/dist/shared/components-gyt7my41.js +0 -328
- package/dist/shared/components-h6p86be3.js +0 -19
- package/dist/shared/components-h83az0bz.js +0 -33
- package/dist/shared/components-hdzrqqpx.js +0 -55
- package/dist/shared/components-krtx3m5b.js +0 -84
- package/dist/shared/components-kswxnm3h.js +0 -24
- package/dist/shared/components-mpa2y6x8.js +0 -277
- package/dist/shared/components-mrbaxt8q.js +0 -62
- package/dist/shared/components-pt5ftm4p.js +0 -38
- package/dist/shared/components-rpw4md2b.js +0 -179
- package/dist/shared/components-rtadhd9j.js +0 -64
- package/dist/shared/components-tz4gaf1v.js +0 -23
- package/dist/shared/components-wbbckbw8.js +0 -34
- package/dist/shared/components-xdqmt6ed.js +0 -20
- package/dist/shared/components-xjkdvnbg.js +0 -107
- package/dist/shared/components-z3v0n0de.js +0 -11
- package/dist/shared/components-z6dfqtyt.js +0 -1545
- package/dist/tokens/colors.css +0 -230
- package/dist/tokens/motion.css +0 -40
- package/dist/tokens/radius.css +0 -10
- package/dist/tokens/spacing.css +0 -9
- package/dist/tokens/typography.css +0 -119
- package/docs/chrome.css +0 -120
- package/docs/chrome.tsx +0 -283
- package/docs/index.ts +0 -9
- package/docs/vite.ts +0 -85
- package/index.ts +0 -60
- package/lib/date-locale.ts +0 -195
- package/lib/field-classes.ts +0 -121
- package/lib/field-variants.ts +0 -117
- package/lib/fluid-hover-highlight.tsx +0 -139
- package/lib/focus-ring.tsx +0 -46
- package/lib/goo-indicator.tsx +0 -261
- package/lib/icon-context.tsx +0 -310
- package/lib/inert.ts +0 -22
- package/lib/morph-layers.tsx +0 -102
- package/lib/morph-part.tsx +0 -49
- package/lib/popup.ts +0 -52
- package/lib/reduced-motion.ts +0 -19
- package/lib/row-index.tsx +0 -29
- package/lib/row-order.tsx +0 -126
- package/lib/scroll-column.tsx +0 -273
- package/lib/shape-context.tsx +0 -138
- package/lib/shape-radius.ts +0 -67
- package/lib/size-context.tsx +0 -66
- package/lib/size-scale.ts +0 -115
- package/lib/slot.ts +0 -115
- package/lib/springs.ts +0 -62
- package/lib/surface-classes.ts +0 -58
- package/lib/surface-context.tsx +0 -21
- package/lib/use-controllable-state.ts +0 -44
- package/lib/use-fluid-hover.ts +0 -541
- package/lib/use-is-mobile.ts +0 -25
- package/lib/use-keyboard-nav-gate.ts +0 -45
- package/lib/use-merge-split.tsx +0 -209
- package/lib/use-morph.ts +0 -557
- package/lib/use-touch-primary.tsx +0 -43
- package/lib/utils.ts +0 -37
- package/preset.ts +0 -199
- package/primitives/code/examples.mdx +0 -27
- package/primitives/code/index.tsx +0 -80
- package/primitives/craft/demos.tsx +0 -173
- package/primitives/craft/examples.mdx +0 -142
- package/primitives/craft/index.tsx +0 -76
- package/primitives/fluid-hover/examples.mdx +0 -84
- package/primitives/fluid-hover/index.tsx +0 -316
- package/primitives/morph/examples.mdx +0 -139
- package/primitives/morph/index.tsx +0 -192
- package/primitives/motion/examples.mdx +0 -100
- package/primitives/motion/index.tsx +0 -160
- package/primitives/scroll-area/examples.mdx +0 -58
- package/primitives/scroll-area/index.tsx +0 -204
- package/primitives/separator/examples.mdx +0 -30
- package/primitives/separator/index.tsx +0 -53
- package/primitives/sizes/examples.mdx +0 -167
- package/primitives/sizes/index.tsx +0 -89
- package/primitives/skeleton/examples.mdx +0 -51
- package/primitives/skeleton/index.tsx +0 -72
- package/primitives/skeleton/skeleton.css +0 -15
- package/primitives/surface/examples.mdx +0 -173
- package/primitives/surface/index.tsx +0 -109
- package/primitives/surface/surface.css +0 -176
- /package/dist/shared/{components-3ey0hzpb.js → components-1v7rxytx.js} +0 -0
- /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
|