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