@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,49 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Local addition (not part of Fluid Functionalism): shared parts for morphing
|
|
3
|
+
* overlays. A part inside the trigger and its twin inside the overlay share a
|
|
4
|
+
* motion `layoutId`, so the part flies from one to the other while the surface
|
|
5
|
+
* morphs, like motion-primitives' morphing dialog image and title
|
|
6
|
+
* (github.com/ibelick/motion-primitives `components/core/morphing-dialog.tsx`
|
|
7
|
+
* @ 120f64f6ca60348e251f929e9c81f11ccbe45eda — MIT © 2024 ibelick, notice:
|
|
8
|
+
* LICENSE.motion-primitives).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { motion } from 'motion/react'
|
|
12
|
+
import { createContext, type ReactNode, useContext } from 'react'
|
|
13
|
+
import { useReduceMotion } from './reduced-motion'
|
|
14
|
+
import { spring } from './springs'
|
|
15
|
+
|
|
16
|
+
/** Set by an overlay root: the id its parts are scoped to and whether it is open. */
|
|
17
|
+
const MorphPartScope = createContext<{ id: string; open: boolean } | null>(null)
|
|
18
|
+
|
|
19
|
+
/** True inside an overlay's surface (`MorphSurface` provides it). */
|
|
20
|
+
const MorphPartInOverlay = createContext(false)
|
|
21
|
+
|
|
22
|
+
const { exit: _exit, ...enter } = spring.slow
|
|
23
|
+
|
|
24
|
+
interface MorphPartProps {
|
|
25
|
+
/** Pairs the part in the trigger with its twin in the overlay; unique within one overlay. */
|
|
26
|
+
id: string
|
|
27
|
+
/** Classes for the part's box. */
|
|
28
|
+
className?: string
|
|
29
|
+
/** The part's content: an image, a title. */
|
|
30
|
+
children?: ReactNode
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function MorphPart({ id, className, children }: MorphPartProps) {
|
|
34
|
+
const scope = useContext(MorphPartScope)
|
|
35
|
+
const inOverlay = useContext(MorphPartInOverlay)
|
|
36
|
+
const reduced = useReduceMotion()
|
|
37
|
+
// The overlay's twin lets go of the id as soon as the close starts, so the
|
|
38
|
+
// trigger's part flies back while Base UI still shows the closing overlay.
|
|
39
|
+
const layoutId = scope && !reduced && (scope.open || !inOverlay) ? `${scope.id}-${id}` : undefined
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<motion.div layoutId={layoutId} transition={enter} className={className}>
|
|
43
|
+
{children}
|
|
44
|
+
</motion.div>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type { MorphPartProps }
|
|
49
|
+
export { MorphPart, MorphPartInOverlay, MorphPartScope }
|
package/lib/popup.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Vendored from Fluid Functionalism — `registry/default/lib/popup.ts` at
|
|
3
|
+
* github.com/mickadesign/fluid-functionalism@b3587bdbd83fc66c2a6aae3817ffb856cb09260b
|
|
4
|
+
* (fluidfunctionalism.com). MIT License © 2026 Micka Touillaud — see
|
|
5
|
+
* LICENSE.fluid-functionalism in this package.
|
|
6
|
+
*
|
|
7
|
+
* Local modifications:
|
|
8
|
+
* - `popupMotionClass` (the side-aware transform origin and slide offset of
|
|
9
|
+
* the old `scaleY` enter/exit) dropped: the dropdown, select and combobox
|
|
10
|
+
* popups now morph out of their trigger through `lib/use-morph.ts`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Shared popup chrome for the dropdown, select, and combobox popups: the
|
|
15
|
+
// ScrollArea sizing the list scrolls inside. One definition, so the three
|
|
16
|
+
// popups never drift apart.
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Popup lists scroll inside ScrollArea: the hover-revealed thumb instead of
|
|
21
|
+
* the platform scrollbar, and a scroll-aware fade on the viewport. The popup
|
|
22
|
+
* is only max-height constrained, so the viewport's percentage height can't
|
|
23
|
+
* resolve — it inherits the max-height instead (the ScrollArea root inherits
|
|
24
|
+
* it from the popup) and sizes to its rows up to that. The scroll primitive's
|
|
25
|
+
* inline-styled sizer is forced back to a plain block so rows can shrink and
|
|
26
|
+
* truncate; the `[style]` qualifier keeps that off the rows' own wrapper on
|
|
27
|
+
* touch devices, where ScrollArea renders no sizer.
|
|
28
|
+
*/
|
|
29
|
+
export const popupScrollAreaClass = 'min-h-0 flex-1 max-h-[inherit]'
|
|
30
|
+
export const popupViewportClass =
|
|
31
|
+
'!h-auto max-h-[inherit] [&>div[style]]:!block [&>div[style]]:!min-w-0 [--scroll-fade-size:32px]'
|
|
32
|
+
|
|
33
|
+
/** Keys that count as keyboard navigation inside a popup (earn the ring). */
|
|
34
|
+
export const POPUP_NAV_KEYS = [
|
|
35
|
+
'ArrowDown',
|
|
36
|
+
'ArrowUp',
|
|
37
|
+
'ArrowLeft',
|
|
38
|
+
'ArrowRight',
|
|
39
|
+
'Home',
|
|
40
|
+
'End',
|
|
41
|
+
'PageUp',
|
|
42
|
+
'PageDown',
|
|
43
|
+
'Tab',
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Rows the fluid hover must skip: a disabled row is neither a target
|
|
48
|
+
* nor a hover stop, whichever attribute the primitive marks it with.
|
|
49
|
+
*/
|
|
50
|
+
export function isDisabledRow(el: HTMLElement): boolean {
|
|
51
|
+
return el.getAttribute('aria-disabled') === 'true' || el.hasAttribute('data-disabled')
|
|
52
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Local addition (not part of Fluid Functionalism): the one reduced-motion
|
|
3
|
+
* read for the morph layer and the liquid indicators.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useReducedMotion, useReducedMotionConfig } from 'motion/react'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Reduce motion when the OS asks for it or a surrounding `MotionConfig` does.
|
|
10
|
+
* motion's `useReducedMotionConfig` alone follows the OS only under
|
|
11
|
+
* `<MotionConfig reducedMotion="user">`; without that wrapper it defaults to
|
|
12
|
+
* "never", so a page that sets none would animate for a user who turned
|
|
13
|
+
* motion off.
|
|
14
|
+
*/
|
|
15
|
+
export function useReduceMotion() {
|
|
16
|
+
const os = useReducedMotion() ?? false
|
|
17
|
+
const config = useReducedMotionConfig() ?? false
|
|
18
|
+
return os || config
|
|
19
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Local addition (not part of Fluid Functionalism): numbers a group's rows in
|
|
3
|
+
* order (Checkbox.Group, Radio.Group, Switch.Group), so a row needs no
|
|
4
|
+
* `index` for the fluid hover and the group's index-keyed state.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Children, createContext, isValidElement, type ReactNode, useContext } from 'react'
|
|
8
|
+
|
|
9
|
+
const RowIndexContext = createContext(0)
|
|
10
|
+
|
|
11
|
+
/** Gives each element child its position among them. */
|
|
12
|
+
function IndexedRows({ children }: { children: ReactNode }) {
|
|
13
|
+
let index = 0
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
{Children.map(children, child =>
|
|
17
|
+
isValidElement(child) ? <RowIndexContext.Provider value={index++}>{child}</RowIndexContext.Provider> : child,
|
|
18
|
+
)}
|
|
19
|
+
</>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A row's index: its own `index`, else its position in the group. */
|
|
24
|
+
function useRowIndex(own?: number) {
|
|
25
|
+
const position = useContext(RowIndexContext)
|
|
26
|
+
return own ?? position
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { IndexedRows, useRowIndex }
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Local addition (not part of Fluid Functionalism): numbers a menu's rows in
|
|
3
|
+
* document order, so a `Dropdown.Item` needs no `index` for the fluid hover,
|
|
4
|
+
* the keyboard ring and the selection backgrounds. Unlike `lib/row-index.tsx`
|
|
5
|
+
* (direct children only) it reaches rows anywhere below the panel: inside a
|
|
6
|
+
* `Dropdown.Group`, behind a condition, out of a `map`. Labels, separators and
|
|
7
|
+
* groups take a number too without registering a rect, so a selection run
|
|
8
|
+
* never melts across them.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
createContext,
|
|
13
|
+
type RefObject,
|
|
14
|
+
useCallback,
|
|
15
|
+
useContext,
|
|
16
|
+
useId,
|
|
17
|
+
useLayoutEffect,
|
|
18
|
+
useMemo,
|
|
19
|
+
useReducer,
|
|
20
|
+
useRef,
|
|
21
|
+
useState,
|
|
22
|
+
} from 'react'
|
|
23
|
+
|
|
24
|
+
interface Slot {
|
|
25
|
+
element: HTMLElement
|
|
26
|
+
/** The row's own `index`; the numbering starts after the highest one. */
|
|
27
|
+
explicit?: number
|
|
28
|
+
/** A label, separator or group: numbered, never a row. */
|
|
29
|
+
gap?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface RowOrder {
|
|
33
|
+
register: (id: string, slot: Slot) => () => void
|
|
34
|
+
order: ReadonlyMap<string, number>
|
|
35
|
+
/** The first row's number: the inline panel's roving tab stop. */
|
|
36
|
+
first?: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const EMPTY: ReadonlyMap<string, number> = new Map()
|
|
40
|
+
|
|
41
|
+
const RowOrderContext = createContext<RowOrder | null>(null)
|
|
42
|
+
|
|
43
|
+
const before = (a: Slot, b: Slot) =>
|
|
44
|
+
a.element.compareDocumentPosition(b.element) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1
|
|
45
|
+
|
|
46
|
+
function number(slots: Map<string, Slot>) {
|
|
47
|
+
let next = 0
|
|
48
|
+
for (const slot of slots.values()) if (slot.explicit !== undefined) next = Math.max(next, slot.explicit + 1)
|
|
49
|
+
const order = new Map<string, number>()
|
|
50
|
+
let first: number | undefined
|
|
51
|
+
for (const [id, slot] of [...slots].sort(([, a], [, b]) => before(a, b))) {
|
|
52
|
+
if (slot.explicit !== undefined) continue
|
|
53
|
+
if (!slot.gap) first ??= next
|
|
54
|
+
order.set(id, next++)
|
|
55
|
+
}
|
|
56
|
+
return { order, first }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface Numbering {
|
|
60
|
+
order: ReadonlyMap<string, number>
|
|
61
|
+
first?: number
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function same({ order: a, first: firstA }: Numbering, { order: b, first: firstB }: Numbering) {
|
|
65
|
+
if (firstA !== firstB || a.size !== b.size) return false
|
|
66
|
+
for (const [id, index] of a) if (b.get(id) !== index) return false
|
|
67
|
+
return true
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A panel's numbering. Provide the result through `RowOrderContext`. */
|
|
71
|
+
function useRowOrder(): RowOrder {
|
|
72
|
+
const slots = useRef(new Map<string, Slot>())
|
|
73
|
+
const [numbering, setNumbering] = useState<Numbering>({ order: EMPTY })
|
|
74
|
+
const [, changed] = useReducer((n: number) => n + 1, 0)
|
|
75
|
+
|
|
76
|
+
const register = useCallback((id: string, slot: Slot) => {
|
|
77
|
+
slots.current.set(id, slot)
|
|
78
|
+
changed()
|
|
79
|
+
return () => {
|
|
80
|
+
slots.current.delete(id)
|
|
81
|
+
changed()
|
|
82
|
+
}
|
|
83
|
+
}, [])
|
|
84
|
+
|
|
85
|
+
// Every render: a re-ordered list moves no row in or out, so only the DOM
|
|
86
|
+
// knows. Rows register in their own layout effects, which run first.
|
|
87
|
+
useLayoutEffect(() => {
|
|
88
|
+
const next = number(slots.current)
|
|
89
|
+
setNumbering(prev => (same(prev, next) ? prev : next))
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
return useMemo(() => ({ register, ...numbering }), [register, numbering])
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Takes a place in the surrounding panel's order and returns the row's index
|
|
97
|
+
* (`explicit` when given, else its position; undefined until the first layout
|
|
98
|
+
* pass, and outside a panel) and the panel's first row.
|
|
99
|
+
*/
|
|
100
|
+
function useRowSlot(ref: RefObject<HTMLElement | null>, explicit?: number, gap?: boolean) {
|
|
101
|
+
const ctx = useContext(RowOrderContext)
|
|
102
|
+
const id = useId()
|
|
103
|
+
const register = ctx?.register
|
|
104
|
+
|
|
105
|
+
useLayoutEffect(() => {
|
|
106
|
+
const element = ref.current
|
|
107
|
+
if (!register || !element) return
|
|
108
|
+
return register(id, { element, explicit, gap })
|
|
109
|
+
}, [register, id, explicit, gap, ref])
|
|
110
|
+
|
|
111
|
+
return { index: explicit ?? ctx?.order.get(id), firstRow: ctx?.first }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type { RowOrder }
|
|
115
|
+
export { RowOrderContext, useRowOrder, useRowSlot }
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Local addition (not part of Fluid Functionalism): the scrolling option list
|
|
3
|
+
* the date and time pickers stack beside their calendar (months, years,
|
|
4
|
+
* hours, minutes), or lay out as a strip above it on a narrow panel.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { type KeyboardEvent, type ReactNode, useEffect, useRef } from 'react'
|
|
8
|
+
import { FluidHoverHighlight } from './fluid-hover-highlight'
|
|
9
|
+
import { useReduceMotion } from './reduced-motion'
|
|
10
|
+
import { useShape } from './shape-context'
|
|
11
|
+
import { useSize } from './size-context'
|
|
12
|
+
import { useFluidHover, useRegisterFluidHoverItem } from './use-fluid-hover'
|
|
13
|
+
import { cn } from './utils'
|
|
14
|
+
|
|
15
|
+
interface ScrollColumnOption<T extends number | string> {
|
|
16
|
+
value: T
|
|
17
|
+
label: ReactNode
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ScrollColumnProps<T extends number | string> {
|
|
22
|
+
/** Accessible name of the list ("Month", "Hour"). */
|
|
23
|
+
label: string
|
|
24
|
+
options: ScrollColumnOption<T>[]
|
|
25
|
+
/** The picked option: a dot and a heavier weight. */
|
|
26
|
+
value: T | null
|
|
27
|
+
onValueChange: (value: T) => void
|
|
28
|
+
/** Options to band, as a scrollbar thumb would (the months the calendar shows). */
|
|
29
|
+
inView?: (value: T) => boolean
|
|
30
|
+
/** The option kept centred. Defaults to `value`. */
|
|
31
|
+
follow?: T | null
|
|
32
|
+
/** A column, or a strip on a narrow panel. Defaults to `'vertical'`. */
|
|
33
|
+
orientation?: 'vertical' | 'horizontal'
|
|
34
|
+
className?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Scrolls `list` so `item` sits in its middle, on whichever axis it scrolls.
|
|
39
|
+
* Measured in layout pixels: a panel still scaled by its opening morph
|
|
40
|
+
* reports shrunken client rects.
|
|
41
|
+
*/
|
|
42
|
+
function centre(list: HTMLElement, item: HTMLElement, behavior: ScrollBehavior) {
|
|
43
|
+
const l = list.getBoundingClientRect()
|
|
44
|
+
const i = item.getBoundingClientRect()
|
|
45
|
+
const sx = l.width / list.offsetWidth || 1
|
|
46
|
+
const sy = l.height / list.offsetHeight || 1
|
|
47
|
+
list.scrollTo({
|
|
48
|
+
top: list.scrollTop + (i.top - l.top) / sy - (list.clientHeight - item.offsetHeight) / 2,
|
|
49
|
+
left: list.scrollLeft + (i.left - l.left) / sx - (list.clientWidth - item.offsetWidth) / 2,
|
|
50
|
+
behavior,
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** For a row of columns: a hairline between neighbours. */
|
|
55
|
+
const dividedColumns = 'flex [&>*+*]:border-s [&>*]:border-border'
|
|
56
|
+
|
|
57
|
+
const STEP: Record<string, number> = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }
|
|
58
|
+
|
|
59
|
+
function ScrollColumn<T extends number | string>({
|
|
60
|
+
label,
|
|
61
|
+
options,
|
|
62
|
+
value,
|
|
63
|
+
onValueChange,
|
|
64
|
+
inView,
|
|
65
|
+
follow = value,
|
|
66
|
+
orientation = 'vertical',
|
|
67
|
+
className,
|
|
68
|
+
}: ScrollColumnProps<T>) {
|
|
69
|
+
const listRef = useRef<HTMLDivElement>(null)
|
|
70
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
71
|
+
const pointerInside = useRef(false)
|
|
72
|
+
const centred = useRef(false)
|
|
73
|
+
const reduceMotion = useReduceMotion()
|
|
74
|
+
const horizontal = orientation === 'horizontal'
|
|
75
|
+
const hover = useFluidHover(containerRef, { axis: horizontal ? 'x' : 'y' })
|
|
76
|
+
const shape = useShape()
|
|
77
|
+
|
|
78
|
+
// Recentres only when the followed option (or the axis) changes.
|
|
79
|
+
const axis = useRef(horizontal)
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const list = listRef.current
|
|
82
|
+
const item = list?.querySelector<HTMLElement>(`[data-value="${follow}"]`)
|
|
83
|
+
if (!list || !item || pointerInside.current) return
|
|
84
|
+
// A jump when the list opens or turns: it lands on its option.
|
|
85
|
+
const glide = centred.current && axis.current === horizontal && !reduceMotion
|
|
86
|
+
centre(list, item, glide ? 'smooth' : 'instant')
|
|
87
|
+
centred.current = true
|
|
88
|
+
axis.current = horizontal
|
|
89
|
+
}, [follow, horizontal])
|
|
90
|
+
|
|
91
|
+
// A resize (the panel settling its width, a strip turning into a column)
|
|
92
|
+
// moves the followed option off centre: put it back at once.
|
|
93
|
+
const followed = useRef(follow)
|
|
94
|
+
followed.current = follow
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
const list = listRef.current
|
|
97
|
+
if (!list) return
|
|
98
|
+
const observer = new ResizeObserver(() => {
|
|
99
|
+
const item = list.querySelector<HTMLElement>(`[data-value="${followed.current}"]`)
|
|
100
|
+
if (item && !pointerInside.current) centre(list, item, 'instant')
|
|
101
|
+
})
|
|
102
|
+
observer.observe(list)
|
|
103
|
+
return () => observer.disconnect()
|
|
104
|
+
}, [])
|
|
105
|
+
|
|
106
|
+
// A run of banded options reads as one band: only its ends are rounded.
|
|
107
|
+
const banded = options.map(o => inView?.(o.value) ?? false)
|
|
108
|
+
|
|
109
|
+
const tabStop = options.some(o => o.value === value) ? value : (follow ?? options[0]?.value)
|
|
110
|
+
|
|
111
|
+
const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
|
112
|
+
const items = [
|
|
113
|
+
...(containerRef.current?.querySelectorAll<HTMLButtonElement>('[role="option"]:not(:disabled)') ?? []),
|
|
114
|
+
]
|
|
115
|
+
const at = items.indexOf(document.activeElement as HTMLButtonElement)
|
|
116
|
+
const next =
|
|
117
|
+
e.key === 'Home' ? 0 : e.key === 'End' ? items.length - 1 : STEP[e.key] ? at + (STEP[e.key] ?? 0) : undefined
|
|
118
|
+
if (next === undefined) return
|
|
119
|
+
e.preventDefault()
|
|
120
|
+
const item = items[Math.max(0, Math.min(items.length - 1, next))]
|
|
121
|
+
item?.focus({ preventScroll: true })
|
|
122
|
+
if (item && listRef.current) centre(listRef.current, item, reduceMotion ? 'instant' : 'smooth')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div
|
|
127
|
+
ref={listRef}
|
|
128
|
+
role="listbox"
|
|
129
|
+
aria-label={label}
|
|
130
|
+
aria-orientation={orientation}
|
|
131
|
+
onKeyDown={onKeyDown}
|
|
132
|
+
onPointerEnter={() => {
|
|
133
|
+
pointerInside.current = true
|
|
134
|
+
}}
|
|
135
|
+
onPointerLeave={() => {
|
|
136
|
+
pointerInside.current = false
|
|
137
|
+
}}
|
|
138
|
+
className={cn(
|
|
139
|
+
'scrollbar-hide [--scroll-fade-size:24px]',
|
|
140
|
+
horizontal
|
|
141
|
+
? 'overflow-x-auto overflow-y-hidden scroll-fade-x'
|
|
142
|
+
: 'overflow-y-auto overflow-x-hidden scroll-fade',
|
|
143
|
+
className,
|
|
144
|
+
)}
|
|
145
|
+
>
|
|
146
|
+
{/* biome-ignore lint/a11y/noStaticElementInteractions lint/a11y/useKeyWithClickEvents: the fluid hover tracks the pointer here; the options inside are the buttons. */}
|
|
147
|
+
<div
|
|
148
|
+
ref={containerRef}
|
|
149
|
+
onMouseMove={hover.handlers.onMouseMove}
|
|
150
|
+
onMouseEnter={hover.handlers.onMouseEnter}
|
|
151
|
+
onMouseLeave={hover.handlers.onMouseLeave}
|
|
152
|
+
onClick={hover.handlers.onClick}
|
|
153
|
+
className={cn('relative flex p-1', horizontal ? 'w-max flex-row' : 'flex-col')}
|
|
154
|
+
>
|
|
155
|
+
<FluidHoverHighlight hover={hover} className={shape.bg} />
|
|
156
|
+
{options.map((option, index) => (
|
|
157
|
+
<ScrollColumnItem
|
|
158
|
+
key={option.value}
|
|
159
|
+
index={index}
|
|
160
|
+
registerItem={hover.registerItem}
|
|
161
|
+
option={option}
|
|
162
|
+
selected={option.value === value}
|
|
163
|
+
band={
|
|
164
|
+
!banded[index]
|
|
165
|
+
? undefined
|
|
166
|
+
: banded[index - 1]
|
|
167
|
+
? banded[index + 1]
|
|
168
|
+
? 'middle'
|
|
169
|
+
: 'end'
|
|
170
|
+
: banded[index + 1]
|
|
171
|
+
? 'start'
|
|
172
|
+
: 'single'
|
|
173
|
+
}
|
|
174
|
+
horizontal={horizontal}
|
|
175
|
+
tabStop={option.value === tabStop}
|
|
176
|
+
onSelect={() => onValueChange(option.value)}
|
|
177
|
+
/>
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function ScrollColumnItem<T extends number | string>({
|
|
185
|
+
index,
|
|
186
|
+
registerItem,
|
|
187
|
+
option,
|
|
188
|
+
selected,
|
|
189
|
+
band,
|
|
190
|
+
horizontal,
|
|
191
|
+
tabStop,
|
|
192
|
+
onSelect,
|
|
193
|
+
}: {
|
|
194
|
+
index: number
|
|
195
|
+
registerItem: (index: number, element: HTMLElement | null) => void
|
|
196
|
+
option: ScrollColumnOption<T>
|
|
197
|
+
selected: boolean
|
|
198
|
+
band?: 'start' | 'middle' | 'end' | 'single'
|
|
199
|
+
horizontal: boolean
|
|
200
|
+
tabStop: boolean
|
|
201
|
+
onSelect: () => void
|
|
202
|
+
}) {
|
|
203
|
+
const ref = useRef<HTMLButtonElement>(null)
|
|
204
|
+
useRegisterFluidHoverItem(registerItem, index, ref)
|
|
205
|
+
const shape = useShape()
|
|
206
|
+
const sizeClasses = useSize()
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<button
|
|
210
|
+
ref={ref}
|
|
211
|
+
type="button"
|
|
212
|
+
role="option"
|
|
213
|
+
aria-selected={selected}
|
|
214
|
+
tabIndex={tabStop ? 0 : -1}
|
|
215
|
+
disabled={option.disabled}
|
|
216
|
+
data-value={option.value}
|
|
217
|
+
data-fluid-hover-index={index}
|
|
218
|
+
onClick={onSelect}
|
|
219
|
+
className={cn(
|
|
220
|
+
'relative z-[1] flex shrink-0 items-center justify-center px-2 tabular-nums whitespace-nowrap outline-none',
|
|
221
|
+
'transition-[color,background-color] duration-fast focus-visible:ring-1 focus-visible:ring-focus-ring',
|
|
222
|
+
'disabled:opacity-50 disabled:pointer-events-none',
|
|
223
|
+
sizeClasses.segmentItem,
|
|
224
|
+
sizeClasses.text,
|
|
225
|
+
shape.item,
|
|
226
|
+
// FF's tint for a passive region (Accordion's open row); the pick is Select's checked row.
|
|
227
|
+
band && !selected && 'bg-accent/20 dark:bg-accent/12',
|
|
228
|
+
selected && 'bg-active',
|
|
229
|
+
band && band !== 'single' && 'rounded-none',
|
|
230
|
+
band === 'start' && (horizontal ? 'rounded-s-lg' : 'rounded-t-lg'),
|
|
231
|
+
band === 'end' && (horizontal ? 'rounded-e-lg' : 'rounded-b-lg'),
|
|
232
|
+
selected ? 'text-foreground weight-semibold' : band ? 'text-foreground' : 'text-muted-foreground',
|
|
233
|
+
)}
|
|
234
|
+
>
|
|
235
|
+
{selected && (
|
|
236
|
+
<span aria-hidden className="absolute start-1 top-1/2 size-1 -translate-y-1/2 rounded-full bg-current" />
|
|
237
|
+
)}
|
|
238
|
+
{option.label}
|
|
239
|
+
</button>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export type { ScrollColumnOption, ScrollColumnProps }
|
|
244
|
+
export { dividedColumns, ScrollColumn }
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// biome-ignore-all lint: vendored upstream code keeps its own patterns (see the header)
|
|
2
|
+
/*
|
|
3
|
+
* Vendored from Fluid Functionalism — `registry/default/lib/shape-context.tsx` at
|
|
4
|
+
* github.com/mickadesign/fluid-functionalism@b3587bdbd83fc66c2a6aae3817ffb856cb09260b
|
|
5
|
+
* (fluidfunctionalism.com). MIT License © 2026 Micka Touillaud — see
|
|
6
|
+
* LICENSE.fluid-functionalism in this package.
|
|
7
|
+
*
|
|
8
|
+
* Local modifications:
|
|
9
|
+
* - `"use client"` directive dropped (no RSC here).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
|
13
|
+
|
|
14
|
+
type ShapeVariant = 'pill' | 'rounded'
|
|
15
|
+
|
|
16
|
+
interface ShapeClasses {
|
|
17
|
+
/** The variant these classes belong to — handy for conditionals. */
|
|
18
|
+
variant: ShapeVariant
|
|
19
|
+
item: string
|
|
20
|
+
bg: string
|
|
21
|
+
focusRing: string
|
|
22
|
+
mergedBg: string
|
|
23
|
+
container: string
|
|
24
|
+
button: string
|
|
25
|
+
input: string
|
|
26
|
+
// Numeric counterparts of `bg` / `mergedBg`, in px. Needed where individual
|
|
27
|
+
// corners are animated (e.g. the selected-background merge/split animation),
|
|
28
|
+
// which requires per-corner numeric border-radii rather than a class.
|
|
29
|
+
bgRadius: number
|
|
30
|
+
mergedRadius: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const shapeMap: Record<ShapeVariant, ShapeClasses> = {
|
|
34
|
+
pill: {
|
|
35
|
+
variant: 'pill',
|
|
36
|
+
item: 'rounded-[20px]',
|
|
37
|
+
bg: 'rounded-[20px]',
|
|
38
|
+
// +2px over `item` because the focus ring sits 2px outside the element
|
|
39
|
+
// (top/left -2, width/height +4); this keeps the corners concentric so a
|
|
40
|
+
// pill element gets a pill ring (matches the rounded-mode 8px→10px bump).
|
|
41
|
+
focusRing: 'rounded-[22px]',
|
|
42
|
+
mergedBg: 'rounded-2xl',
|
|
43
|
+
container: 'rounded-3xl',
|
|
44
|
+
button: 'rounded-[20px]',
|
|
45
|
+
input: 'rounded-[20px]',
|
|
46
|
+
bgRadius: 20,
|
|
47
|
+
mergedRadius: 16,
|
|
48
|
+
},
|
|
49
|
+
rounded: {
|
|
50
|
+
variant: 'rounded',
|
|
51
|
+
item: 'rounded-lg',
|
|
52
|
+
bg: 'rounded-lg',
|
|
53
|
+
focusRing: 'rounded-[10px]',
|
|
54
|
+
mergedBg: 'rounded-lg',
|
|
55
|
+
container: 'rounded-xl',
|
|
56
|
+
button: 'rounded-lg',
|
|
57
|
+
input: 'rounded-lg',
|
|
58
|
+
bgRadius: 8,
|
|
59
|
+
mergedRadius: 8,
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface ShapeContextValue {
|
|
64
|
+
shape: ShapeVariant
|
|
65
|
+
setShape: (shape: ShapeVariant) => void
|
|
66
|
+
classes: ShapeClasses
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const ShapeContext = createContext<ShapeContextValue | null>(null)
|
|
70
|
+
|
|
71
|
+
// Rounded is the default on every path: the site demos render under
|
|
72
|
+
// <ShapeProvider defaultShape="rounded">, the shipped :focus-visible fallback
|
|
73
|
+
// ring assumes its 8px radius, and the preset generators only emit a provider
|
|
74
|
+
// for pill. A consumer with no provider gets the corners the docs show.
|
|
75
|
+
function useShape(): ShapeClasses {
|
|
76
|
+
const ctx = useContext(ShapeContext)
|
|
77
|
+
if (!ctx) return shapeMap.rounded
|
|
78
|
+
return ctx.classes
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function useShapeContext() {
|
|
82
|
+
const ctx = useContext(ShapeContext)
|
|
83
|
+
if (!ctx) throw new Error('useShapeContext must be used within a ShapeProvider')
|
|
84
|
+
return ctx
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function ShapeProvider({ children, defaultShape = 'rounded' }: { children: ReactNode; defaultShape?: ShapeVariant }) {
|
|
88
|
+
const [shape, setShapeState] = useState<ShapeVariant>(defaultShape)
|
|
89
|
+
const transitionTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
90
|
+
|
|
91
|
+
// Run a state change under the `.transitioning` guard (added + reflow-flushed
|
|
92
|
+
// first so the 180ms border-radius cross-fade applies). Clearing the previous
|
|
93
|
+
// timeout first keeps a double-press from removing the class mid-fade.
|
|
94
|
+
const transitionShape = useCallback((callback: () => void) => {
|
|
95
|
+
const root = document.documentElement
|
|
96
|
+
root.classList.add('transitioning')
|
|
97
|
+
void root.offsetHeight
|
|
98
|
+
callback()
|
|
99
|
+
if (transitionTimeoutRef.current) clearTimeout(transitionTimeoutRef.current)
|
|
100
|
+
transitionTimeoutRef.current = setTimeout(() => root.classList.remove('transitioning'), 200)
|
|
101
|
+
}, [])
|
|
102
|
+
|
|
103
|
+
const setShape = useCallback(
|
|
104
|
+
(next: ShapeVariant) => {
|
|
105
|
+
transitionShape(() => setShapeState(next))
|
|
106
|
+
},
|
|
107
|
+
[transitionShape],
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
// Publish the current element radius as a CSS custom property so plain-CSS
|
|
111
|
+
// consumers that can't read React context stay in sync with the shape
|
|
112
|
+
// system — e.g. the @layer base :focus-visible fallback ring in
|
|
113
|
+
// globals.css. Set on <html> so portalled content sees it too.
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
document.documentElement.style.setProperty('--shape-input-radius', `${shapeMap[shape].bgRadius}px`)
|
|
116
|
+
}, [shape])
|
|
117
|
+
|
|
118
|
+
const value = useMemo(() => ({ shape, setShape, classes: shapeMap[shape] }), [shape, setShape])
|
|
119
|
+
|
|
120
|
+
return <ShapeContext.Provider value={value}>{children}</ShapeContext.Provider>
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type { ShapeClasses, ShapeVariant }
|
|
124
|
+
export { ShapeProvider, shapeMap, useShape, useShapeContext }
|