@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,474 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Local addition (not part of Fluid Functionalism): gooey toasts after Sileo
|
|
3
|
+
* (github.com/hiaaryan/sileo `src/sileo.tsx` + `src/styles.css` @
|
|
4
|
+
* 9793f844349983e140cf33cebbb8f51626d41407 — MIT, notice: LICENSE.sileo). A
|
|
5
|
+
* pill with the state's icon and title melts into a body with the description
|
|
6
|
+
* and an action, through the shared goo filter; the body opens a beat after
|
|
7
|
+
* the toast lands and folds back before it leaves (Sileo's autopilot), or
|
|
8
|
+
* whenever it is hovered. Base UI's Toast owns the queue, timers, swipe to
|
|
9
|
+
* dismiss and announcements; Sileo's own store, timers and swipe are not
|
|
10
|
+
* ported. The toast is drawn like every other morph surface: the Elevated
|
|
11
|
+
* level's background shapes melted by the shared goo filter, their shadows
|
|
12
|
+
* beneath, the content on top. Colors are the status tokens, the sizes the
|
|
13
|
+
* control ladder's, and the motion the spring tiers; reduced motion keeps the
|
|
14
|
+
* fades and snaps the shapes. The global `toast()` (`manager.ts`) feeds a
|
|
15
|
+
* `Toast` mounted with `manager={toast}` from anywhere, React or not; titles
|
|
16
|
+
* and descriptions take any node, `icon` replaces the state's glyph and
|
|
17
|
+
* `action` takes a button's `{ label, onClick }` or any node. The state
|
|
18
|
+
* glyphs come from the icon set (`useIcons`), so an `IconProvider` swaps them.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { type ToastManager, Toast as ToastPrimitive } from '@base-ui/react/toast'
|
|
22
|
+
import { AnimatePresence, motion } from 'motion/react'
|
|
23
|
+
import {
|
|
24
|
+
cloneElement,
|
|
25
|
+
createContext,
|
|
26
|
+
forwardRef,
|
|
27
|
+
isValidElement,
|
|
28
|
+
type ReactNode,
|
|
29
|
+
type Ref,
|
|
30
|
+
useContext,
|
|
31
|
+
useEffect,
|
|
32
|
+
useId,
|
|
33
|
+
useLayoutEffect,
|
|
34
|
+
useMemo,
|
|
35
|
+
useRef,
|
|
36
|
+
useState,
|
|
37
|
+
} from 'react'
|
|
38
|
+
import { type IconComponent, type IconName, useIcons } from '../../../lib/icon-context'
|
|
39
|
+
import { GooFilter } from '../../../lib/morph-layers'
|
|
40
|
+
import { useReduceMotion } from '../../../lib/reduced-motion'
|
|
41
|
+
import { useShape } from '../../../lib/shape-context'
|
|
42
|
+
import { sizeMap } from '../../../lib/size-context'
|
|
43
|
+
import { type SlotProps, slotRender } from '../../../lib/slot'
|
|
44
|
+
import { spring } from '../../../lib/springs'
|
|
45
|
+
import { SURFACE_BG, SURFACE_SHADOW } from '../../../lib/surface-classes'
|
|
46
|
+
import { SurfaceProvider, useSurface } from '../../../lib/surface-context'
|
|
47
|
+
import { GOO_BLUR_RATIO, holdExit } from '../../../lib/use-morph'
|
|
48
|
+
import { cn } from '../../../lib/utils'
|
|
49
|
+
import Button from '../../button'
|
|
50
|
+
import {
|
|
51
|
+
type ToastActionOptions,
|
|
52
|
+
type ToastData,
|
|
53
|
+
type ToastHandle,
|
|
54
|
+
type ToastInput,
|
|
55
|
+
type ToastOptions,
|
|
56
|
+
type ToastPromiseOptions,
|
|
57
|
+
type ToastSource,
|
|
58
|
+
type ToastType,
|
|
59
|
+
toast,
|
|
60
|
+
toManagerOptions,
|
|
61
|
+
} from './manager'
|
|
62
|
+
|
|
63
|
+
type ToastPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
|
|
64
|
+
|
|
65
|
+
const ICONS: Record<ToastType, IconName> = {
|
|
66
|
+
success: 'check',
|
|
67
|
+
loading: 'loader-circle',
|
|
68
|
+
error: 'x',
|
|
69
|
+
warning: 'circle-alert',
|
|
70
|
+
info: 'info',
|
|
71
|
+
action: 'arrow-right',
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Literal per state so the class extractor sees each one.
|
|
75
|
+
const TONES: Record<ToastType, string> = {
|
|
76
|
+
success: 'text-status-success bg-status-success/16',
|
|
77
|
+
loading: 'text-status-loading bg-status-loading/16',
|
|
78
|
+
error: 'text-status-error bg-status-error/16',
|
|
79
|
+
warning: 'text-status-warning bg-status-warning/16',
|
|
80
|
+
info: 'text-status-info bg-status-info/16',
|
|
81
|
+
action: 'text-status-action bg-status-action/16',
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const VIEWPORT: Record<ToastPosition, string> = {
|
|
85
|
+
'top-left': 'top-4 left-4 items-start',
|
|
86
|
+
'top-center': 'top-4 left-1/2 -translate-x-1/2 items-center',
|
|
87
|
+
'top-right': 'top-4 right-4 items-end',
|
|
88
|
+
'bottom-left': 'bottom-4 left-4 flex-col-reverse items-start',
|
|
89
|
+
'bottom-center': 'bottom-4 left-1/2 -translate-x-1/2 flex-col-reverse items-center',
|
|
90
|
+
'bottom-right': 'bottom-4 right-4 flex-col-reverse items-end',
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// The pill is one control tall, and the goo melts as much as its radius allows.
|
|
94
|
+
const PILL = sizeMap.default.controlHeight
|
|
95
|
+
const BLUR = (PILL / 2) * GOO_BLUR_RATIO
|
|
96
|
+
const SNAP = { duration: 0 }
|
|
97
|
+
const AUTO_EXPAND_MS = 150
|
|
98
|
+
const AUTO_COLLAPSE_BEFORE_MS = 2000
|
|
99
|
+
|
|
100
|
+
const ToastSettings = createContext<{ position: ToastPosition; timeout: number }>({
|
|
101
|
+
position: 'top-right',
|
|
102
|
+
timeout: 6000,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Shows toasts from inside a `Toast`: `show`, the per-state shortcuts,
|
|
107
|
+
* `promise`, `update` and `close` (alias `dismiss`), plus the `toasts` showing.
|
|
108
|
+
* Under a `Toast` with `manager={toast}` it works on the same stack as the
|
|
109
|
+
* global `toast()`.
|
|
110
|
+
*/
|
|
111
|
+
function useToast() {
|
|
112
|
+
const manager = ToastPrimitive.useToastManager<ToastData>()
|
|
113
|
+
|
|
114
|
+
return useMemo(() => {
|
|
115
|
+
const show = (o: ToastInput) => manager.add({ type: 'info', ...toManagerOptions(o) })
|
|
116
|
+
const typed = (type: ToastType) => (o: ToastInput) =>
|
|
117
|
+
show(typeof o === 'string' ? { title: o, type } : { ...o, type })
|
|
118
|
+
const result = <T,>(r: ToastPromiseOptions<T>['success']) =>
|
|
119
|
+
typeof r === 'function' ? (value: T) => toManagerOptions(r(value)) : toManagerOptions(r)
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
toasts: manager.toasts,
|
|
123
|
+
show,
|
|
124
|
+
success: typed('success'),
|
|
125
|
+
error: typed('error'),
|
|
126
|
+
warning: typed('warning'),
|
|
127
|
+
info: typed('info'),
|
|
128
|
+
promise: <T,>(promise: Promise<T>, o: ToastPromiseOptions<T>) =>
|
|
129
|
+
manager.promise(promise, {
|
|
130
|
+
loading: toManagerOptions(o.loading),
|
|
131
|
+
success: result(o.success),
|
|
132
|
+
error: result(o.error),
|
|
133
|
+
}),
|
|
134
|
+
update: (id: string, o: ToastInput) => manager.update(id, toManagerOptions(o)),
|
|
135
|
+
close: manager.close,
|
|
136
|
+
dismiss: manager.close,
|
|
137
|
+
}
|
|
138
|
+
}, [manager])
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface Box {
|
|
142
|
+
x: number
|
|
143
|
+
y: number
|
|
144
|
+
width: number
|
|
145
|
+
height: number
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function Shape({ box, className, shapeRef }: { box: Box; className: string; shapeRef?: Ref<HTMLDivElement> }) {
|
|
149
|
+
const reduced = useReduceMotion()
|
|
150
|
+
return (
|
|
151
|
+
<motion.div
|
|
152
|
+
ref={shapeRef}
|
|
153
|
+
aria-hidden
|
|
154
|
+
className={cn('pointer-events-none absolute top-0 left-0', className)}
|
|
155
|
+
initial={false}
|
|
156
|
+
animate={{ ...box }}
|
|
157
|
+
transition={reduced ? SNAP : spring.slow}
|
|
158
|
+
/>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The pill's glyph: the state's icon, or the toast's own `icon` (a component, or any node). */
|
|
163
|
+
function StatusIcon({ type, icon }: { type: ToastType; icon: ToastData['icon'] }) {
|
|
164
|
+
const icons = useIcons()
|
|
165
|
+
// A component is a function or a forwardRef / memo object; an element is a node.
|
|
166
|
+
const component =
|
|
167
|
+
typeof icon === 'function' ||
|
|
168
|
+
(typeof icon === 'object' && icon !== null && '$$typeof' in icon && !isValidElement(icon))
|
|
169
|
+
if (icon != null && icon !== false && !component) return <>{icon}</>
|
|
170
|
+
const Icon = component ? (icon as IconComponent) : icons[ICONS[type]]
|
|
171
|
+
return <Icon size={14} strokeWidth={2.5} className={type === 'loading' && !component ? 'animate-spin' : undefined} />
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function ToastItem({ toast }: { toast: ToastPrimitive.Root.ToastObject<ToastData> }) {
|
|
175
|
+
const { position, timeout } = useContext(ToastSettings)
|
|
176
|
+
const shape = useShape()
|
|
177
|
+
// Lifts 2 levels off its substrate with a fixed shadow, like a popover.
|
|
178
|
+
const level = Math.min(useSurface() + 2, 8)
|
|
179
|
+
const gooId = `toast-goo-${useId().replace(/:/g, '')}`
|
|
180
|
+
const type = (toast.type ?? 'info') as ToastType
|
|
181
|
+
const customAction = toast.data?.action
|
|
182
|
+
const top = position.startsWith('top')
|
|
183
|
+
const ending = toast.transitionStatus === 'ending'
|
|
184
|
+
const hasBody = Boolean(toast.description || toast.actionProps || customAction)
|
|
185
|
+
const [hovered, setHovered] = useState(false)
|
|
186
|
+
const [auto, setAuto] = useState(false)
|
|
187
|
+
const expanded = hasBody && type !== 'loading' && !ending && (hovered || auto)
|
|
188
|
+
const root = useRef<HTMLDivElement>(null)
|
|
189
|
+
const header = useRef<HTMLDivElement>(null)
|
|
190
|
+
const body = useRef<HTMLDivElement>(null)
|
|
191
|
+
const bodyShape = useRef<HTMLDivElement>(null)
|
|
192
|
+
const [size, setSize] = useState({ width: 0, pill: PILL, body: 0, radius: 0 })
|
|
193
|
+
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (!hasBody || type === 'loading') return
|
|
196
|
+
const life = toast.timeout ?? timeout
|
|
197
|
+
const open = setTimeout(() => setAuto(true), AUTO_EXPAND_MS)
|
|
198
|
+
const close =
|
|
199
|
+
life > 0 ? setTimeout(() => setAuto(false), Math.max(AUTO_EXPAND_MS, life - AUTO_COLLAPSE_BEFORE_MS)) : undefined
|
|
200
|
+
return () => {
|
|
201
|
+
clearTimeout(open)
|
|
202
|
+
clearTimeout(close)
|
|
203
|
+
}
|
|
204
|
+
}, [hasBody, type, toast.timeout, timeout])
|
|
205
|
+
|
|
206
|
+
// The header and the body content keep their natural size (the header is
|
|
207
|
+
// `w-max`, outside the pill), so measuring them never feeds back the
|
|
208
|
+
// animated shapes. A body that appears later (a promise's result) is
|
|
209
|
+
// observed from then on.
|
|
210
|
+
useLayoutEffect(() => {
|
|
211
|
+
const measure = () =>
|
|
212
|
+
setSize({
|
|
213
|
+
width: root.current?.offsetWidth ?? 0,
|
|
214
|
+
pill: Math.max(PILL, header.current?.offsetWidth ?? 0),
|
|
215
|
+
body: body.current?.offsetHeight ?? 0,
|
|
216
|
+
radius: bodyShape.current ? Number.parseFloat(getComputedStyle(bodyShape.current).borderTopLeftRadius) || 0 : 0,
|
|
217
|
+
})
|
|
218
|
+
const observer = new ResizeObserver(measure)
|
|
219
|
+
for (const el of [header.current, body.current]) if (el) observer.observe(el)
|
|
220
|
+
measure()
|
|
221
|
+
return () => observer.disconnect()
|
|
222
|
+
}, [hasBody])
|
|
223
|
+
|
|
224
|
+
useLayoutEffect(() => {
|
|
225
|
+
if (!ending || !root.current) return
|
|
226
|
+
const hold = holdExit(root.current, 'moderate')
|
|
227
|
+
return () => hold.cancel()
|
|
228
|
+
}, [ending])
|
|
229
|
+
|
|
230
|
+
// The pill sits on the toast's side of the screen; the body opens away from
|
|
231
|
+
// the edge. While open, the pill reaches into the body by the body's corner
|
|
232
|
+
// radius, covering that corner, so the outer edge runs straight and the goo
|
|
233
|
+
// only rounds the inner corner (Sileo's open pill). The body's padding on
|
|
234
|
+
// the pill side clears that reach. The header and the body content ride the
|
|
235
|
+
// same values as their shapes, so they never drift apart mid-spring.
|
|
236
|
+
const bodyHeight = expanded ? size.body : 0
|
|
237
|
+
const height = PILL + bodyHeight
|
|
238
|
+
const pillX = position.endsWith('left')
|
|
239
|
+
? 0
|
|
240
|
+
: position.endsWith('right')
|
|
241
|
+
? size.width - size.pill
|
|
242
|
+
: (size.width - size.pill) / 2
|
|
243
|
+
const pillHeight = PILL + Math.min(bodyHeight, size.radius)
|
|
244
|
+
const pill = { x: pillX, y: top ? 0 : height - pillHeight, width: size.pill, height: pillHeight }
|
|
245
|
+
const bodyBox = { x: 0, y: top ? PILL : 0, width: size.width, height: bodyHeight }
|
|
246
|
+
const reduced = useReduceMotion()
|
|
247
|
+
// Reduced motion keeps the fades and drops the travel.
|
|
248
|
+
const away = reduced ? { opacity: 0 } : { opacity: 0, y: top ? -6 : 6, scale: 0.95 }
|
|
249
|
+
const move = reduced ? SNAP : spring.slow
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<ToastPrimitive.Root
|
|
253
|
+
ref={root}
|
|
254
|
+
toast={toast}
|
|
255
|
+
swipeDirection={[top ? 'up' : 'down', position.endsWith('left') ? 'left' : 'right']}
|
|
256
|
+
onMouseEnter={() => setHovered(true)}
|
|
257
|
+
onMouseLeave={() => setHovered(false)}
|
|
258
|
+
render={
|
|
259
|
+
<motion.div
|
|
260
|
+
initial={{ ...away, height: PILL }}
|
|
261
|
+
animate={ending ? { ...away, height } : { opacity: 1, y: 0, scale: 1, height }}
|
|
262
|
+
transition={ending ? spring.moderate.exit : { ...spring.moderate, height: move }}
|
|
263
|
+
/>
|
|
264
|
+
}
|
|
265
|
+
className="relative w-[min(22rem,calc(100vw-2rem))] select-none outline-none [translate:var(--toast-swipe-movement-x)_var(--toast-swipe-movement-y)]"
|
|
266
|
+
>
|
|
267
|
+
<GooFilter id={gooId} blur={BLUR} />
|
|
268
|
+
<Shape box={pill} className={cn('rounded-full', SURFACE_SHADOW[3])} />
|
|
269
|
+
{hasBody && <Shape box={bodyBox} className={cn(shape.container, SURFACE_SHADOW[3])} />}
|
|
270
|
+
{/* The filter is the effect itself: it melts the two token-filled shapes into one. */}
|
|
271
|
+
<div aria-hidden className="pointer-events-none absolute inset-0" style={{ filter: `url(#${gooId})` }}>
|
|
272
|
+
<Shape box={pill} className={cn('rounded-full', SURFACE_BG[level])} />
|
|
273
|
+
{hasBody && <Shape box={bodyBox} shapeRef={bodyShape} className={cn(shape.container, SURFACE_BG[level])} />}
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<SurfaceProvider value={level}>
|
|
277
|
+
<motion.div
|
|
278
|
+
ref={header}
|
|
279
|
+
className="absolute top-0 left-0 flex h-9 w-max items-center py-1.5 pr-4 pl-1.5"
|
|
280
|
+
initial={false}
|
|
281
|
+
animate={{ x: pillX, y: top ? 0 : height - PILL }}
|
|
282
|
+
transition={move}
|
|
283
|
+
>
|
|
284
|
+
<ToastPrimitive.Title className="relative inline-flex items-center whitespace-nowrap">
|
|
285
|
+
<AnimatePresence mode="popLayout" initial={false}>
|
|
286
|
+
<motion.span
|
|
287
|
+
// A string title keys itself; a node title crossfades on each update.
|
|
288
|
+
key={`${type}-${typeof toast.title === 'string' ? toast.title : toast.updateKey}`}
|
|
289
|
+
className="inline-flex items-center gap-2"
|
|
290
|
+
initial={{ opacity: 0, filter: 'blur(4px)' }}
|
|
291
|
+
animate={{ opacity: 1, filter: 'blur(0px)' }}
|
|
292
|
+
exit={{ opacity: 0, filter: 'blur(4px)', transition: spring.moderate.exit }}
|
|
293
|
+
transition={spring.moderate}
|
|
294
|
+
>
|
|
295
|
+
<span className={cn('flex size-6 items-center justify-center rounded-full', TONES[type])}>
|
|
296
|
+
<StatusIcon type={type} icon={toast.data?.icon} />
|
|
297
|
+
</span>
|
|
298
|
+
<span className="text-body weight-medium text-foreground">{toast.title}</span>
|
|
299
|
+
</motion.span>
|
|
300
|
+
</AnimatePresence>
|
|
301
|
+
</ToastPrimitive.Title>
|
|
302
|
+
</motion.div>
|
|
303
|
+
{hasBody && (
|
|
304
|
+
<motion.div
|
|
305
|
+
className="absolute top-0 left-0 w-full overflow-hidden"
|
|
306
|
+
initial={false}
|
|
307
|
+
animate={{ y: bodyBox.y, height: bodyHeight, opacity: expanded ? 1 : 0 }}
|
|
308
|
+
transition={{ ...move, opacity: expanded ? spring.slow : spring.slow.exit }}
|
|
309
|
+
>
|
|
310
|
+
<div ref={body} className={cn('flex flex-col items-start gap-3 p-4', top ? 'pt-6' : 'pb-6')}>
|
|
311
|
+
<ToastPrimitive.Description className="text-caption text-muted-foreground" />
|
|
312
|
+
{toast.actionProps && <ToastPrimitive.Action render={<Button size="compact" variant="secondary" />} />}
|
|
313
|
+
{customAction}
|
|
314
|
+
</div>
|
|
315
|
+
</motion.div>
|
|
316
|
+
)}
|
|
317
|
+
</SurfaceProvider>
|
|
318
|
+
</ToastPrimitive.Root>
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function ToastList() {
|
|
323
|
+
const { toasts } = ToastPrimitive.useToastManager<ToastData>()
|
|
324
|
+
return toasts.map(toast => <ToastItem key={toast.id} toast={toast} />)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
interface ToastTriggerProps extends Omit<SlotProps, 'type' | 'title'> {
|
|
328
|
+
/** The toast's title, in the pill. */
|
|
329
|
+
title?: ReactNode
|
|
330
|
+
/** Text for the body the pill melts into. */
|
|
331
|
+
description?: ReactNode
|
|
332
|
+
/** State: sets the icon and its color. Defaults to `'info'`. */
|
|
333
|
+
type?: 'success' | 'loading' | 'error' | 'warning' | 'info' | 'action'
|
|
334
|
+
/** Ms before it leaves; 0 keeps it until dismissed. Defaults to the `Toast`'s `timeout`. */
|
|
335
|
+
timeout?: number
|
|
336
|
+
/** Replaces the state's icon in the pill: an icon component, or any node. */
|
|
337
|
+
icon?: IconComponent | ReactNode
|
|
338
|
+
/** A button in the body (`{ label, onClick }`), or any node. */
|
|
339
|
+
action?: ToastActionOptions | ReactNode
|
|
340
|
+
/** Runs a task: the toast shows `title` while it is pending, then `success` or `error`. */
|
|
341
|
+
promise?: () => Promise<unknown>
|
|
342
|
+
/** The toast once `promise` resolves. */
|
|
343
|
+
success?: ToastOptions
|
|
344
|
+
/** The toast if `promise` rejects. */
|
|
345
|
+
error?: ToastOptions
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const ToastTrigger = forwardRef<HTMLButtonElement, ToastTriggerProps>(
|
|
349
|
+
(
|
|
350
|
+
{
|
|
351
|
+
title,
|
|
352
|
+
description,
|
|
353
|
+
type,
|
|
354
|
+
timeout,
|
|
355
|
+
icon,
|
|
356
|
+
action,
|
|
357
|
+
promise,
|
|
358
|
+
success,
|
|
359
|
+
error,
|
|
360
|
+
render,
|
|
361
|
+
asChild,
|
|
362
|
+
children,
|
|
363
|
+
onClick,
|
|
364
|
+
...props
|
|
365
|
+
},
|
|
366
|
+
ref,
|
|
367
|
+
) => {
|
|
368
|
+
const toasts = useToast()
|
|
369
|
+
const el = slotRender(render, asChild, children)
|
|
370
|
+
const press = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
371
|
+
onClick?.(event)
|
|
372
|
+
if (promise) {
|
|
373
|
+
// The error toast is the report; the rejection Base UI hands back would only surface unhandled.
|
|
374
|
+
toasts
|
|
375
|
+
.promise(promise(), { loading: { title, description, icon }, success: success ?? {}, error: error ?? {} })
|
|
376
|
+
.catch(() => {})
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
toasts.show({ title, description, type, timeout, icon, action })
|
|
380
|
+
}
|
|
381
|
+
return el ? (
|
|
382
|
+
cloneElement(el, { ...props, ref, onClick: press })
|
|
383
|
+
) : (
|
|
384
|
+
<button ref={ref} type="button" {...props} onClick={press}>
|
|
385
|
+
{children}
|
|
386
|
+
</button>
|
|
387
|
+
)
|
|
388
|
+
},
|
|
389
|
+
)
|
|
390
|
+
ToastTrigger.displayName = 'ToastTrigger'
|
|
391
|
+
|
|
392
|
+
interface ToastProps {
|
|
393
|
+
/** Where toasts stack; the pill sits on that side and the body opens away from the edge. Defaults to `'top-right'`. */
|
|
394
|
+
position?: 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
|
|
395
|
+
/** How many toasts show at once; older ones wait. Defaults to `3`. */
|
|
396
|
+
limit?: number
|
|
397
|
+
/** Ms a toast stays; hovering pauses it. Defaults to `6000`. */
|
|
398
|
+
timeout?: number
|
|
399
|
+
/** Also renders the toasts this manager shows: pass the global `toast` so `toast()` calls from anywhere land here (mount one such `Toast` per app), or a manager from Base UI's `Toast.createToastManager()`. */
|
|
400
|
+
manager?: ToastSource
|
|
401
|
+
/** The part of the app that can show toasts, with `Toast.Trigger`s or `useToast()`. Optional: a `Toast` with a `manager` can stand alone. */
|
|
402
|
+
children?: ReactNode
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Gooey toasts: a pill with the state's icon and title that melts into a body
|
|
407
|
+
* with the details.
|
|
408
|
+
*
|
|
409
|
+
* Each toast lands as a pill one control tall; when it has a description or
|
|
410
|
+
* an action, the pill melts into a body a beat later through the shared goo
|
|
411
|
+
* filter (see Morph), and folds back before it leaves, or stays open while
|
|
412
|
+
* hovered. Six states (`success`, `loading`, `error`, `warning`, `info`,
|
|
413
|
+
* `action`) set the icon and its status color; a promise toast spins as
|
|
414
|
+
* `loading` and crossfades into its result. Built on Base UI's Toast:
|
|
415
|
+
* timers pause on hover, a swipe toward the edge dismisses, and screen
|
|
416
|
+
* readers hear every toast.
|
|
417
|
+
*
|
|
418
|
+
* `Toast` wraps the part of the app that shows toasts and renders them at
|
|
419
|
+
* its `position`. Show one with `Toast.Trigger`, or call `useToast()`
|
|
420
|
+
* inside it: `show`, `success`, `error`, `warning`, `info`, `promise`,
|
|
421
|
+
* `update` and `close`.
|
|
422
|
+
*
|
|
423
|
+
* Or mount one `<Toast manager={toast} />` near the root and call the global
|
|
424
|
+
* `toast()` from anywhere, inside React or not: `toast({ title, description,
|
|
425
|
+
* type, timeout, icon, action, onClose })`, `toast.success`, `.error`,
|
|
426
|
+
* `.warning`, `.info`, `.loading`, `.promise`, `.update(id, …)` and
|
|
427
|
+
* `.dismiss(id?)`. Each call returns a handle with the toast's `id`,
|
|
428
|
+
* `update` and `dismiss`; calls made before the `Toast` mounts wait for it.
|
|
429
|
+
* Titles and descriptions take any node, `icon` replaces the state's glyph,
|
|
430
|
+
* and `action` is a button (`{ label, onClick }`) or any node.
|
|
431
|
+
*
|
|
432
|
+
* Statics:
|
|
433
|
+
* - `Toast.Trigger` — a control that shows a toast when pressed:
|
|
434
|
+
* `render={<Button/>}` or `asChild`, with the toast's `title`,
|
|
435
|
+
* `description`, `type`, `timeout`, `action`, or a `promise` with its
|
|
436
|
+
* `success` / `error` toasts.
|
|
437
|
+
*
|
|
438
|
+
* @example {@include ./examples.mdx}
|
|
439
|
+
*/
|
|
440
|
+
function Toast({ position = 'top-right', limit = 3, timeout = 6000, manager, children }: ToastProps) {
|
|
441
|
+
const settings = useMemo(() => ({ position, timeout }), [position, timeout])
|
|
442
|
+
|
|
443
|
+
return (
|
|
444
|
+
// The provider only ever subscribes to the manager, which is all a `ToastSource` promises.
|
|
445
|
+
<ToastPrimitive.Provider limit={limit} timeout={timeout} toastManager={manager as ToastManager | undefined}>
|
|
446
|
+
<ToastSettings.Provider value={settings}>
|
|
447
|
+
{children}
|
|
448
|
+
<ToastPrimitive.Portal>
|
|
449
|
+
<ToastPrimitive.Viewport className={cn('fixed z-[60] flex flex-col gap-2 outline-none', VIEWPORT[position])}>
|
|
450
|
+
<ToastList />
|
|
451
|
+
</ToastPrimitive.Viewport>
|
|
452
|
+
</ToastPrimitive.Portal>
|
|
453
|
+
</ToastSettings.Provider>
|
|
454
|
+
</ToastPrimitive.Provider>
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
Toast.Trigger = ToastTrigger
|
|
459
|
+
|
|
460
|
+
export type {
|
|
461
|
+
ToastActionOptions,
|
|
462
|
+
ToastHandle,
|
|
463
|
+
ToastInput,
|
|
464
|
+
ToastOptions,
|
|
465
|
+
ToastPosition,
|
|
466
|
+
ToastPromiseOptions,
|
|
467
|
+
ToastProps,
|
|
468
|
+
ToastSource,
|
|
469
|
+
ToastTriggerProps,
|
|
470
|
+
ToastType,
|
|
471
|
+
}
|
|
472
|
+
export { Toast, ToastTrigger, toast, useToast }
|
|
473
|
+
|
|
474
|
+
export default Toast
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Local addition (not part of Fluid Functionalism): the toast options and the
|
|
3
|
+
* global `toast()` that works outside React. One module-level Base UI toast
|
|
4
|
+
* manager (`Toast.createToastManager`) carries every call to the `Toast`
|
|
5
|
+
* mounted with `manager={toast}`; calls made before that `Toast` subscribes
|
|
6
|
+
* (a child's mount effect runs before its parent's) wait in a queue and play
|
|
7
|
+
* once it does, so no toast is lost on the first render. On the server (no
|
|
8
|
+
* `window`) nothing will ever subscribe, so calls there are dropped instead of
|
|
9
|
+
* queued for the life of the process.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { type ToastManager, Toast as ToastPrimitive } from '@base-ui/react/toast'
|
|
13
|
+
import { isValidElement, type ReactNode } from 'react'
|
|
14
|
+
import type { IconComponent } from '../../../lib/icon-context'
|
|
15
|
+
|
|
16
|
+
type ToastType = 'success' | 'loading' | 'error' | 'warning' | 'info' | 'action'
|
|
17
|
+
|
|
18
|
+
/** A button in the toast's body. */
|
|
19
|
+
interface ToastActionOptions {
|
|
20
|
+
label: ReactNode
|
|
21
|
+
onClick?: () => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ToastOptions {
|
|
25
|
+
/** Reuse an id to replace a toast that is still showing. */
|
|
26
|
+
id?: string
|
|
27
|
+
title?: ReactNode
|
|
28
|
+
description?: ReactNode
|
|
29
|
+
type?: ToastType
|
|
30
|
+
/** Ms before it leaves; 0 keeps it until dismissed. */
|
|
31
|
+
timeout?: number
|
|
32
|
+
/** Same as `timeout`; `timeout` wins when both are set. */
|
|
33
|
+
duration?: number
|
|
34
|
+
/** Replaces the state's icon in the pill: an icon component, or any node. */
|
|
35
|
+
icon?: IconComponent | ReactNode
|
|
36
|
+
/** A button in the body (`{ label, onClick }`), or any node. */
|
|
37
|
+
action?: ToastActionOptions | ReactNode
|
|
38
|
+
/** Runs once the toast starts to leave, however it was closed. */
|
|
39
|
+
onClose?: () => void
|
|
40
|
+
/** `high` is announced at once by screen readers; `low` waits its turn. */
|
|
41
|
+
priority?: 'low' | 'high'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** What a toast keeps beside Base UI's own fields. */
|
|
45
|
+
interface ToastData {
|
|
46
|
+
icon?: IconComponent | ReactNode
|
|
47
|
+
action?: ReactNode
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type ToastInput = string | ToastOptions
|
|
51
|
+
type ToastResult<T> = ToastInput | ((value: T) => ToastInput)
|
|
52
|
+
|
|
53
|
+
interface ToastPromiseOptions<T> {
|
|
54
|
+
loading: ToastInput
|
|
55
|
+
success: ToastResult<T>
|
|
56
|
+
error: ToastResult<unknown>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A shown toast: its id, and updating or dismissing it. */
|
|
60
|
+
interface ToastHandle {
|
|
61
|
+
id: string
|
|
62
|
+
update: (options: ToastInput) => void
|
|
63
|
+
dismiss: () => void
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const isActionButton = (action: unknown): action is ToastActionOptions =>
|
|
67
|
+
typeof action === 'object' && action !== null && !isValidElement(action) && 'label' in action
|
|
68
|
+
|
|
69
|
+
/** Drops `undefined` keys: Base UI merges an update over the toast, so a missing key must not clear a field. */
|
|
70
|
+
function defined<T extends object>(value: T): T {
|
|
71
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as T
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Options as Base UI's manager takes them: the action button as `actionProps`, the rest in `data`. */
|
|
75
|
+
function toManagerOptions(input: ToastInput) {
|
|
76
|
+
const { action, icon, duration, timeout, ...rest } = typeof input === 'string' ? { title: input } : input
|
|
77
|
+
const button = isActionButton(action)
|
|
78
|
+
return defined({
|
|
79
|
+
...rest,
|
|
80
|
+
timeout: timeout ?? duration,
|
|
81
|
+
actionProps: button ? { children: action.label, onClick: action.onClick } : undefined,
|
|
82
|
+
data:
|
|
83
|
+
icon !== undefined || (action !== undefined && !button)
|
|
84
|
+
? { icon, action: button ? undefined : action }
|
|
85
|
+
: undefined,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const withType = (input: ToastInput, type: ToastType): ToastOptions =>
|
|
90
|
+
typeof input === 'string' ? { title: input, type } : { ...input, type }
|
|
91
|
+
|
|
92
|
+
const resultOptions = <T>(result: ToastResult<T>) =>
|
|
93
|
+
typeof result === 'function' ? (value: T) => toManagerOptions(result(value)) : toManagerOptions(result)
|
|
94
|
+
|
|
95
|
+
/** Base UI's manager, holding its events until a provider listens. */
|
|
96
|
+
function createQueuedManager(): ToastManager<ToastData> {
|
|
97
|
+
const manager = ToastPrimitive.createToastManager<ToastData>()
|
|
98
|
+
let listeners = 0
|
|
99
|
+
let queue: (() => void)[] = []
|
|
100
|
+
let count = 0
|
|
101
|
+
// No window, no provider to come: drop the call rather than hold it forever.
|
|
102
|
+
const browser = () => typeof window !== 'undefined'
|
|
103
|
+
const whenHeard = (run: () => void) => {
|
|
104
|
+
if (listeners) run()
|
|
105
|
+
else if (browser()) queue.push(run)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
' subscribe': listener => {
|
|
110
|
+
const unsubscribe = manager[' subscribe'](listener)
|
|
111
|
+
listeners++
|
|
112
|
+
const queued = queue
|
|
113
|
+
queue = []
|
|
114
|
+
for (const run of queued) run()
|
|
115
|
+
return () => {
|
|
116
|
+
listeners--
|
|
117
|
+
unsubscribe()
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
add: options => {
|
|
121
|
+
// A known id, so the caller can update or dismiss a toast still queued.
|
|
122
|
+
const id = options.id ?? `toast-global-${++count}`
|
|
123
|
+
whenHeard(() => manager.add({ ...options, id }))
|
|
124
|
+
return id
|
|
125
|
+
},
|
|
126
|
+
close: id => whenHeard(() => manager.close(id)),
|
|
127
|
+
update: (id, updates) => whenHeard(() => manager.update(id, updates)),
|
|
128
|
+
promise: (promise, options) => {
|
|
129
|
+
if (listeners) return manager.promise(promise, options)
|
|
130
|
+
// The toast reports a rejection; the caller still gets the original promise.
|
|
131
|
+
if (browser()) queue.push(() => manager.promise(promise, options).catch(() => {}))
|
|
132
|
+
return promise
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const manager = createQueuedManager()
|
|
138
|
+
|
|
139
|
+
function show(input: ToastInput, type?: ToastType): ToastHandle {
|
|
140
|
+
const options = toManagerOptions(input)
|
|
141
|
+
const id = manager.add({ type: 'info', ...options, ...(type && { type }) })
|
|
142
|
+
return {
|
|
143
|
+
id,
|
|
144
|
+
update: next => manager.update(id, toManagerOptions(next)),
|
|
145
|
+
dismiss: () => manager.close(id),
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Shows a toast from anywhere — an event handler, a fetch wrapper, code
|
|
151
|
+
* outside React — in the `Toast` mounted with `manager={toast}`. Returns a
|
|
152
|
+
* handle with the id, `update` and `dismiss`.
|
|
153
|
+
*/
|
|
154
|
+
const toast = Object.assign((input: ToastInput) => show(input), {
|
|
155
|
+
/** A `success` toast. */
|
|
156
|
+
success: (input: ToastInput) => show(input, 'success'),
|
|
157
|
+
/** An `error` toast. */
|
|
158
|
+
error: (input: ToastInput) => show(input, 'error'),
|
|
159
|
+
/** A `warning` toast. */
|
|
160
|
+
warning: (input: ToastInput) => show(input, 'warning'),
|
|
161
|
+
/** An `info` toast. */
|
|
162
|
+
info: (input: ToastInput) => show(input, 'info'),
|
|
163
|
+
/** A spinning `loading` toast. It stays while its type is `loading`; once updated to another type it leaves on its own `timeout` or the `Toast`'s. */
|
|
164
|
+
loading: (input: ToastInput) => show(withType(input, 'loading')),
|
|
165
|
+
/** Runs `promise` behind a `loading` toast that turns into its `success` or `error` toast. Returns the promise. */
|
|
166
|
+
promise: <T>(promise: Promise<T>, options: ToastPromiseOptions<T>) =>
|
|
167
|
+
manager.promise(promise, {
|
|
168
|
+
loading: toManagerOptions(options.loading),
|
|
169
|
+
success: resultOptions(options.success),
|
|
170
|
+
error: resultOptions(options.error),
|
|
171
|
+
}),
|
|
172
|
+
/** Changes a toast that is showing. */
|
|
173
|
+
update: (id: string, input: ToastInput) => manager.update(id, toManagerOptions(input)),
|
|
174
|
+
/** Closes one toast, or every toast without an id. */
|
|
175
|
+
dismiss: (id?: string) => manager.close(id),
|
|
176
|
+
// Base UI's provider subscribes through this key, so `toast` itself is what `<Toast manager>` takes.
|
|
177
|
+
' subscribe': manager[' subscribe'],
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
/** What `<Toast manager>` takes: the global `toast`, or a manager from Base UI's `Toast.createToastManager()`. */
|
|
181
|
+
type ToastSource = Pick<ToastManager, ' subscribe'>
|
|
182
|
+
|
|
183
|
+
export type {
|
|
184
|
+
ToastActionOptions,
|
|
185
|
+
ToastData,
|
|
186
|
+
ToastHandle,
|
|
187
|
+
ToastInput,
|
|
188
|
+
ToastOptions,
|
|
189
|
+
ToastPromiseOptions,
|
|
190
|
+
ToastSource,
|
|
191
|
+
ToastType,
|
|
192
|
+
}
|
|
193
|
+
export { toast, toManagerOptions }
|