@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,1034 @@
|
|
|
1
|
+
// biome-ignore-all lint: vendored upstream code keeps its own patterns (see the header)
|
|
2
|
+
/*
|
|
3
|
+
* Ported from Fluid Functionalism — github.com/mickadesign/fluid-functionalism
|
|
4
|
+
* `registry/base/accordion.tsx` @ b3587bdbd83fc66c2a6aae3817ffb856cb09260b
|
|
5
|
+
* MIT License © 2026 Micka Touillaud — fluidfunctionalism.com (notice: LICENSE.fluid-functionalism)
|
|
6
|
+
* Local modifications:
|
|
7
|
+
* - `"use client"` dropped; `framer-motion` → `motion/react`; `@/lib/*`,
|
|
8
|
+
* `@/hooks/*` and `@/components/ui/fluid-hover-highlight` → `../../lib/*`,
|
|
9
|
+
* except `SizeProvider` → `../../primitives/sizes`.
|
|
10
|
+
* - Open state (Accordion and AccordionGroup) runs through
|
|
11
|
+
* `useControllableState`, the Base UI contract: `value` controls, otherwise
|
|
12
|
+
* the accordion keeps its own state from `defaultValue` and still reports
|
|
13
|
+
* every change to `onValueChange`. Upstream treated `onValueChange` alone
|
|
14
|
+
* as controlled, so an uncontrolled accordion with a listener never opened.
|
|
15
|
+
* - Standalone `Accordion` accepts `highlight` (the FF API table lists it on
|
|
16
|
+
* the root) and hands it to its items through context as their default;
|
|
17
|
+
* `AccordionItem`'s own `highlight` still wins.
|
|
18
|
+
* - `AccordionItem` hands its ref to Base UI's `Item` instead of overriding the
|
|
19
|
+
* render function's ref, which carries the list-item registration the arrow
|
|
20
|
+
* keys, Home and End read (without it they threw a TypeError).
|
|
21
|
+
* - Prop JSDoc rewritten from the FF docs API tables; modo TSDoc and
|
|
22
|
+
* examples on `Accordion`.
|
|
23
|
+
* - Compound statics `Accordion.Group/.Item/.Trigger/.Content` attached with
|
|
24
|
+
* `Object.assign`, typed through an `AccordionComponent` cast on the
|
|
25
|
+
* forwardRef. Upstream's named exports are kept.
|
|
26
|
+
* - Styling reads DS tokens (AGENTS.md styling): inline
|
|
27
|
+
* `fontVariationSettings` → `weight-*`; the hex focus-ring fallback →
|
|
28
|
+
* `ring-focus-ring` / `border-focus-ring`; `duration-80|120|160` and
|
|
29
|
+
* tier-length JS durations → `duration-<tier>` / `spring.*`.
|
|
30
|
+
* - The panel morphs out of its row instead of springing its height (local
|
|
31
|
+
* morph language, see `primitives/morph`): one progress value grows a shape
|
|
32
|
+
* from the press point in the row (its middle for a keyboard open) to the
|
|
33
|
+
* whole item, the content is revealed by a clip from the same rect, and the
|
|
34
|
+
* panel's layout height follows the shape's bottom edge, so the rows below
|
|
35
|
+
* flow with it. Under `highlight="item"` the open tint is that shape, melted
|
|
36
|
+
* into the row through the shared `GooFilter`; it lives in
|
|
37
|
+
* `AccordionContent` now (the group draws only the `"trigger"` tint), and
|
|
38
|
+
* the hover fills sit one layer above it. The group's hover highlight and
|
|
39
|
+
* focus ring snap with rows a morph pushes around (instead of springing
|
|
40
|
+
* after them over the content) until the pointer or focus moves. `effect`
|
|
41
|
+
* and `tier` on `AccordionContent`; reduced motion snaps, as before.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { Accordion as AccordionPrimitive } from '@base-ui/react/accordion'
|
|
45
|
+
import { AnimatePresence, animate, motion, useMotionValue, useTransform } from 'motion/react'
|
|
46
|
+
import {
|
|
47
|
+
createContext,
|
|
48
|
+
type ForwardRefExoticComponent,
|
|
49
|
+
forwardRef,
|
|
50
|
+
type HTMLAttributes,
|
|
51
|
+
type ReactNode,
|
|
52
|
+
type RefAttributes,
|
|
53
|
+
useCallback,
|
|
54
|
+
useContext,
|
|
55
|
+
useEffect,
|
|
56
|
+
useId,
|
|
57
|
+
useImperativeHandle,
|
|
58
|
+
useLayoutEffect,
|
|
59
|
+
useMemo,
|
|
60
|
+
useRef,
|
|
61
|
+
useState,
|
|
62
|
+
} from 'react'
|
|
63
|
+
|
|
64
|
+
// SSR-safe layout effect (client components still server-render in Next).
|
|
65
|
+
const useIsoLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
|
|
66
|
+
|
|
67
|
+
import { FluidHoverHighlight } from '../../lib/fluid-hover-highlight'
|
|
68
|
+
import { useIcon } from '../../lib/icon-context'
|
|
69
|
+
import { GooFilter } from '../../lib/morph-layers'
|
|
70
|
+
import { useReduceMotion } from '../../lib/reduced-motion'
|
|
71
|
+
import { useShape } from '../../lib/shape-context'
|
|
72
|
+
import { type SizeVariant, useSize } from '../../lib/size-context'
|
|
73
|
+
import { spring } from '../../lib/springs'
|
|
74
|
+
import { useControllableState } from '../../lib/use-controllable-state'
|
|
75
|
+
import { useFluidHover, useRegisterFluidHoverItem } from '../../lib/use-fluid-hover'
|
|
76
|
+
import { GOO_BLUR_RATIO } from '../../lib/use-morph'
|
|
77
|
+
import { cn } from '../../lib/utils'
|
|
78
|
+
import { SizeProvider } from '../../primitives/sizes'
|
|
79
|
+
|
|
80
|
+
// ─── Contexts ────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
interface ItemRect {
|
|
83
|
+
top: number
|
|
84
|
+
left: number
|
|
85
|
+
width: number
|
|
86
|
+
height: number
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface AccordionGroupContextValue {
|
|
90
|
+
registerItem: (index: number, element: HTMLElement | null) => void
|
|
91
|
+
registerFullItem: (index: number, element: HTMLElement | null) => void
|
|
92
|
+
activeIndex: number | null
|
|
93
|
+
grouped: true
|
|
94
|
+
remeasure: () => void
|
|
95
|
+
openValues: Set<string>
|
|
96
|
+
openItemRects: Map<number, ItemRect>
|
|
97
|
+
/** The group's choice; its items draw the `"item"` tint themselves. */
|
|
98
|
+
highlight: 'trigger' | 'item'
|
|
99
|
+
/** A closed row is hovered: open tints step back. */
|
|
100
|
+
dimOpen: boolean
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const AccordionGroupContext = createContext<AccordionGroupContextValue | null>(null)
|
|
104
|
+
|
|
105
|
+
function useAccordionGroup() {
|
|
106
|
+
return useContext(AccordionGroupContext)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface AccordionItemContextValue {
|
|
110
|
+
index?: number
|
|
111
|
+
value: string
|
|
112
|
+
isOpen: boolean
|
|
113
|
+
triggerRef: React.MutableRefObject<HTMLDivElement | null>
|
|
114
|
+
/** Standalone items carry the group's choice themselves. */
|
|
115
|
+
highlight: 'trigger' | 'item'
|
|
116
|
+
/** Where the row was last pressed, in px from its left edge; the panel grows from there. */
|
|
117
|
+
press: React.MutableRefObject<number | null>
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const AccordionItemContext = createContext<AccordionItemContextValue | null>(null)
|
|
121
|
+
|
|
122
|
+
function useAccordionItemContext() {
|
|
123
|
+
const ctx = useContext(AccordionItemContext)
|
|
124
|
+
if (!ctx) throw new Error('AccordionTrigger/AccordionContent must be used within an AccordionItem')
|
|
125
|
+
return ctx
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── AccordionGroup ──────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
type AccordionGroupSingleProps = {
|
|
131
|
+
/** Whether one or multiple items can be expanded. Defaults to `"single"`. */
|
|
132
|
+
type?: 'single'
|
|
133
|
+
/** Controlled expanded value. */
|
|
134
|
+
value?: string
|
|
135
|
+
/** Initially expanded item value — uncontrolled. */
|
|
136
|
+
defaultValue?: string
|
|
137
|
+
/** Callback when expanded state changes. */
|
|
138
|
+
onValueChange?: (value: string) => void
|
|
139
|
+
/** Allow collapsing all items when type is single. Defaults to `true`. */
|
|
140
|
+
collapsible?: boolean
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
type AccordionGroupMultipleProps = {
|
|
144
|
+
/** Whether one or multiple items can be expanded. */
|
|
145
|
+
type: 'multiple'
|
|
146
|
+
/** Controlled expanded values. */
|
|
147
|
+
value?: string[]
|
|
148
|
+
/** Initially expanded item values — uncontrolled. */
|
|
149
|
+
defaultValue?: string[]
|
|
150
|
+
/** Callback when expanded state changes. */
|
|
151
|
+
onValueChange?: (value: string[]) => void
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
type AccordionGroupProps = HTMLAttributes<HTMLDivElement> & {
|
|
155
|
+
children: ReactNode
|
|
156
|
+
/** Pins the group's rows to one step of the size ladder (default 36px,
|
|
157
|
+
* compact 28px — see /docs/sizes). Omitted, they follow the surrounding
|
|
158
|
+
* SizeProvider. */
|
|
159
|
+
size?: SizeVariant
|
|
160
|
+
/** What an open item tints. "item" paints the row and its panel as one
|
|
161
|
+
* block, and holds while it stays open. "trigger" scopes the fill to the
|
|
162
|
+
* row and shows it on hover only, leaving the panel on the page's own
|
|
163
|
+
* surface — the way a sidebar row highlights without colouring its
|
|
164
|
+
* sub-tree. @default "item" */
|
|
165
|
+
highlight?: 'trigger' | 'item'
|
|
166
|
+
} & (AccordionGroupSingleProps | AccordionGroupMultipleProps)
|
|
167
|
+
|
|
168
|
+
const AccordionGroup = forwardRef<HTMLDivElement, AccordionGroupProps>((props, ref) => {
|
|
169
|
+
const { children, highlight = 'item', type = 'single', size, className, ...rest } = props
|
|
170
|
+
|
|
171
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
172
|
+
const fullItemElementsRef = useRef<Map<number, HTMLElement>>(new Map())
|
|
173
|
+
const [openItemRects, setOpenItemRects] = useState<Map<number, ItemRect>>(new Map())
|
|
174
|
+
const openItemRectsRef = useRef(openItemRects)
|
|
175
|
+
|
|
176
|
+
const hover = useFluidHover(containerRef)
|
|
177
|
+
const { activeIndex, setActiveIndex, itemRects, handlers, registerItem, measureItems } = hover
|
|
178
|
+
|
|
179
|
+
const registerFullItem = useCallback((index: number, element: HTMLElement | null) => {
|
|
180
|
+
if (element) {
|
|
181
|
+
fullItemElementsRef.current.set(index, element)
|
|
182
|
+
} else {
|
|
183
|
+
fullItemElementsRef.current.delete(index)
|
|
184
|
+
}
|
|
185
|
+
}, [])
|
|
186
|
+
|
|
187
|
+
const measureFullItems = useCallback(() => {
|
|
188
|
+
if (!containerRef.current) return
|
|
189
|
+
const next = new Map<number, ItemRect>()
|
|
190
|
+
fullItemElementsRef.current.forEach((el, idx) => {
|
|
191
|
+
next.set(idx, {
|
|
192
|
+
top: el.offsetTop,
|
|
193
|
+
left: el.offsetLeft,
|
|
194
|
+
width: el.offsetWidth,
|
|
195
|
+
height: el.offsetHeight,
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
// Skip the state update when nothing moved (mirrors the fluid hover
|
|
199
|
+
// hook's measureItems guard) — this runs per animation frame via
|
|
200
|
+
// onUpdate, and an unconditional set would invalidate the group
|
|
201
|
+
// context and re-render every item even on no-op remeasures.
|
|
202
|
+
const prev = openItemRectsRef.current
|
|
203
|
+
let changed = prev.size !== next.size
|
|
204
|
+
if (!changed) {
|
|
205
|
+
for (const [idx, r] of next) {
|
|
206
|
+
const p = prev.get(idx)
|
|
207
|
+
if (!p || p.top !== r.top || p.left !== r.left || p.width !== r.width || p.height !== r.height) {
|
|
208
|
+
changed = true
|
|
209
|
+
break
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (!changed) return
|
|
214
|
+
openItemRectsRef.current = next
|
|
215
|
+
setOpenItemRects(next)
|
|
216
|
+
}, [])
|
|
217
|
+
|
|
218
|
+
const sp = props as AccordionGroupSingleProps
|
|
219
|
+
const mp = props as AccordionGroupMultipleProps
|
|
220
|
+
const [singleValue, setSingleValue] = useControllableState<string>(
|
|
221
|
+
type === 'single' ? sp.value : undefined,
|
|
222
|
+
type === 'single' ? (sp.defaultValue ?? '') : '',
|
|
223
|
+
type === 'single' ? sp.onValueChange : undefined,
|
|
224
|
+
)
|
|
225
|
+
const [multipleValue, setMultipleValue] = useControllableState<string[]>(
|
|
226
|
+
type === 'multiple' ? mp.value : undefined,
|
|
227
|
+
type === 'multiple' ? (mp.defaultValue ?? []) : [],
|
|
228
|
+
type === 'multiple' ? mp.onValueChange : undefined,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
const openValuesList: string[] = type === 'multiple' ? multipleValue : singleValue ? [singleValue] : []
|
|
232
|
+
|
|
233
|
+
// Keyed on the joined values so the Set (and the group context value
|
|
234
|
+
// below) keeps a stable identity across re-renders where the open values
|
|
235
|
+
// haven't actually changed.
|
|
236
|
+
const openValuesKey = openValuesList.join(',')
|
|
237
|
+
|
|
238
|
+
const openValues = useMemo(
|
|
239
|
+
() => new Set(openValuesList),
|
|
240
|
+
// Deliberately keyed on the joined string, not the (fresh) array.
|
|
241
|
+
[openValuesKey],
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
measureItems()
|
|
246
|
+
measureFullItems()
|
|
247
|
+
}, [measureItems, measureFullItems, children])
|
|
248
|
+
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
measureItems()
|
|
251
|
+
measureFullItems()
|
|
252
|
+
}, [measureItems, measureFullItems, openValuesKey])
|
|
253
|
+
|
|
254
|
+
const [focusedIndex, setFocusedIndex] = useState<number | null>(null)
|
|
255
|
+
|
|
256
|
+
const focusRect = focusedIndex !== null ? itemRects[focusedIndex] : null
|
|
257
|
+
// "trigger" tints the open row only while you're on it: the panel below
|
|
258
|
+
// already says the item is open, so the fill goes back to being a hover
|
|
259
|
+
// affordance rather than a persistent state. The trigger rects are the
|
|
260
|
+
// ones fluid hover already tracks. Local: the "item" block is drawn by each
|
|
261
|
+
// item's content, as the shape its panel morphs out of.
|
|
262
|
+
const expandedRects =
|
|
263
|
+
highlight === 'trigger'
|
|
264
|
+
? new Map(
|
|
265
|
+
[...openItemRects.keys()].flatMap(idx => {
|
|
266
|
+
const rect = idx === activeIndex ? itemRects[idx] : null
|
|
267
|
+
return rect ? ([[idx, rect]] as [number, ItemRect][]) : []
|
|
268
|
+
}),
|
|
269
|
+
)
|
|
270
|
+
: new Map<number, ItemRect>()
|
|
271
|
+
|
|
272
|
+
const isHoveringNonOpen = activeIndex !== null && !openItemRects.has(activeIndex)
|
|
273
|
+
const shape = useShape()
|
|
274
|
+
|
|
275
|
+
const {
|
|
276
|
+
value: _value,
|
|
277
|
+
defaultValue: _defaultValue,
|
|
278
|
+
onValueChange: _onValueChange,
|
|
279
|
+
collapsible: _collapsible,
|
|
280
|
+
type: _type,
|
|
281
|
+
...htmlProps
|
|
282
|
+
} = rest as Record<string, unknown>
|
|
283
|
+
|
|
284
|
+
// Translate FF API → Base UI Accordion API.
|
|
285
|
+
// Base UI always uses `value: string[]` and a `multiple: boolean`. In
|
|
286
|
+
// single mode we wrap the active value in a single-element array.
|
|
287
|
+
const baseValue: string[] = openValuesList
|
|
288
|
+
|
|
289
|
+
const baseOnValueChange = (next: string[]) => {
|
|
290
|
+
if (type === 'multiple') setMultipleValue(next)
|
|
291
|
+
else setSingleValue(next[0] ?? '')
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Local: rows a morphing panel pushes around carry the hover highlight and
|
|
295
|
+
// the focus ring with them (a snap, not a spring chasing them), until the
|
|
296
|
+
// pointer or the focus moves again.
|
|
297
|
+
const [reflowing, setReflowing] = useState(false)
|
|
298
|
+
const remeasure = useCallback(() => {
|
|
299
|
+
setReflowing(true)
|
|
300
|
+
measureItems()
|
|
301
|
+
measureFullItems()
|
|
302
|
+
}, [measureItems, measureFullItems])
|
|
303
|
+
|
|
304
|
+
// Memoized: the group re-renders on every fluid-hover mousemove; a
|
|
305
|
+
// fresh context object each time would re-render every item with it.
|
|
306
|
+
const groupContextValue = useMemo<AccordionGroupContextValue>(
|
|
307
|
+
() => ({
|
|
308
|
+
registerItem,
|
|
309
|
+
registerFullItem,
|
|
310
|
+
activeIndex,
|
|
311
|
+
grouped: true,
|
|
312
|
+
remeasure,
|
|
313
|
+
openValues,
|
|
314
|
+
openItemRects,
|
|
315
|
+
highlight,
|
|
316
|
+
dimOpen: isHoveringNonOpen,
|
|
317
|
+
}),
|
|
318
|
+
[registerItem, registerFullItem, activeIndex, remeasure, openValues, openItemRects, highlight, isHoveringNonOpen],
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
const group = (
|
|
322
|
+
<AccordionGroupContext.Provider value={groupContextValue}>
|
|
323
|
+
<AccordionPrimitive.Root
|
|
324
|
+
value={baseValue}
|
|
325
|
+
onValueChange={baseOnValueChange}
|
|
326
|
+
multiple={type === 'multiple'}
|
|
327
|
+
render={rootProps => {
|
|
328
|
+
const {
|
|
329
|
+
style: _baseStyle,
|
|
330
|
+
onDrag: _onDrag,
|
|
331
|
+
onDragStart: _onDragStart,
|
|
332
|
+
onDragEnd: _onDragEnd,
|
|
333
|
+
onAnimationStart: _onAnimationStart,
|
|
334
|
+
onAnimationEnd: _onAnimationEnd,
|
|
335
|
+
onAnimationIteration: _onAnimationIteration,
|
|
336
|
+
...restRoot
|
|
337
|
+
} = rootProps as React.HTMLAttributes<HTMLDivElement>
|
|
338
|
+
return (
|
|
339
|
+
<div
|
|
340
|
+
{...restRoot}
|
|
341
|
+
ref={node => {
|
|
342
|
+
;(containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node
|
|
343
|
+
if (typeof ref === 'function') ref(node)
|
|
344
|
+
else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node
|
|
345
|
+
}}
|
|
346
|
+
onMouseEnter={handlers.onMouseEnter}
|
|
347
|
+
onMouseMove={e => {
|
|
348
|
+
setReflowing(false)
|
|
349
|
+
const container = containerRef.current
|
|
350
|
+
if (container) {
|
|
351
|
+
const cRect = container.getBoundingClientRect()
|
|
352
|
+
const layoutH = container.offsetHeight
|
|
353
|
+
const visualH = cRect.height
|
|
354
|
+
const scale = layoutH > 0 ? visualH / layoutH : 1
|
|
355
|
+
const localY = (e.clientY - cRect.top) / scale + container.scrollTop
|
|
356
|
+
for (const [idx, full] of openItemRects) {
|
|
357
|
+
const trigger = itemRects[idx]
|
|
358
|
+
if (!trigger) continue
|
|
359
|
+
const contentTop = trigger.top + trigger.height
|
|
360
|
+
const contentBottom = full.top + full.height
|
|
361
|
+
if (localY >= contentTop && localY <= contentBottom) {
|
|
362
|
+
setActiveIndex(null)
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
handlers.onMouseMove(e)
|
|
368
|
+
}}
|
|
369
|
+
onMouseLeave={handlers.onMouseLeave}
|
|
370
|
+
onFocus={e => {
|
|
371
|
+
setReflowing(false)
|
|
372
|
+
const indexAttr = (e.target as HTMLElement)
|
|
373
|
+
.closest('[data-fluid-hover-index]')
|
|
374
|
+
?.getAttribute('data-fluid-hover-index')
|
|
375
|
+
if (indexAttr != null) {
|
|
376
|
+
const idx = Number(indexAttr)
|
|
377
|
+
setActiveIndex(idx)
|
|
378
|
+
setFocusedIndex((e.target as HTMLElement).matches(':focus-visible') ? idx : null)
|
|
379
|
+
}
|
|
380
|
+
}}
|
|
381
|
+
onBlur={e => {
|
|
382
|
+
if (containerRef.current?.contains(e.relatedTarget as Node)) return
|
|
383
|
+
setFocusedIndex(null)
|
|
384
|
+
setActiveIndex(null)
|
|
385
|
+
}}
|
|
386
|
+
className={cn('relative flex flex-col w-72 max-w-full', className)}
|
|
387
|
+
{...(htmlProps as HTMLAttributes<HTMLDivElement>)}
|
|
388
|
+
>
|
|
389
|
+
{/* Expanded item backgrounds */}
|
|
390
|
+
<AnimatePresence>
|
|
391
|
+
{[...expandedRects.entries()].map(([idx, rect]) => (
|
|
392
|
+
<motion.div
|
|
393
|
+
key={`expanded-${idx}`}
|
|
394
|
+
className={`absolute ${shape.bg} bg-accent/20 dark:bg-accent/12 pointer-events-none`}
|
|
395
|
+
// Fade in from the item's current rect: with initial={false}
|
|
396
|
+
// a newly-opened item's background would pop in at full
|
|
397
|
+
// opacity mid-layout-shift while the previous item's bg is
|
|
398
|
+
// still fading out — reads as a glitch when switching items
|
|
399
|
+
// (especially under /demo's scaled card). Geometry still
|
|
400
|
+
// snaps (duration 0) so the bg hugs the animating item.
|
|
401
|
+
initial={{
|
|
402
|
+
top: rect.top,
|
|
403
|
+
left: rect.left,
|
|
404
|
+
width: rect.width,
|
|
405
|
+
height: rect.height,
|
|
406
|
+
opacity: 0,
|
|
407
|
+
}}
|
|
408
|
+
animate={{
|
|
409
|
+
top: rect.top,
|
|
410
|
+
left: rect.left,
|
|
411
|
+
width: rect.width,
|
|
412
|
+
height: rect.height,
|
|
413
|
+
opacity: isHoveringNonOpen ? 0.7 : 1,
|
|
414
|
+
}}
|
|
415
|
+
exit={{ opacity: 0, transition: spring.moderate.exit }}
|
|
416
|
+
transition={{
|
|
417
|
+
top: { duration: 0 },
|
|
418
|
+
left: { duration: 0 },
|
|
419
|
+
width: { duration: 0 },
|
|
420
|
+
height: { duration: 0 },
|
|
421
|
+
opacity: { duration: spring.moderate.exit.duration },
|
|
422
|
+
}}
|
|
423
|
+
/>
|
|
424
|
+
))}
|
|
425
|
+
</AnimatePresence>
|
|
426
|
+
|
|
427
|
+
{/* Hover background, one layer above the items' open tints */}
|
|
428
|
+
<FluidHoverHighlight
|
|
429
|
+
hover={hover}
|
|
430
|
+
className={cn(shape.bg, 'z-1')}
|
|
431
|
+
transition={reflowing ? false : undefined}
|
|
432
|
+
/>
|
|
433
|
+
|
|
434
|
+
{/* Focus ring */}
|
|
435
|
+
<AnimatePresence>
|
|
436
|
+
{focusRect && (
|
|
437
|
+
<motion.div
|
|
438
|
+
className={`absolute ${shape.focusRing} pointer-events-none z-20 border border-focus-ring`}
|
|
439
|
+
initial={false}
|
|
440
|
+
animate={{
|
|
441
|
+
left: focusRect.left - 2,
|
|
442
|
+
top: focusRect.top - 2,
|
|
443
|
+
width: focusRect.width + 4,
|
|
444
|
+
height: focusRect.height + 4,
|
|
445
|
+
}}
|
|
446
|
+
exit={{ opacity: 0, transition: spring.fast.exit }}
|
|
447
|
+
transition={{
|
|
448
|
+
...(reflowing ? { duration: 0 } : spring.fast),
|
|
449
|
+
opacity: { duration: spring.fast.duration },
|
|
450
|
+
}}
|
|
451
|
+
/>
|
|
452
|
+
)}
|
|
453
|
+
</AnimatePresence>
|
|
454
|
+
|
|
455
|
+
{children}
|
|
456
|
+
</div>
|
|
457
|
+
)
|
|
458
|
+
}}
|
|
459
|
+
/>
|
|
460
|
+
</AccordionGroupContext.Provider>
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
// A size prop pins every row in the group to one ladder step.
|
|
464
|
+
return size ? <SizeProvider size={size}>{group}</SizeProvider> : group
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
AccordionGroup.displayName = 'AccordionGroup'
|
|
468
|
+
|
|
469
|
+
// ─── Accordion (Standalone) ──────────────────────────────────────────────────
|
|
470
|
+
|
|
471
|
+
interface AccordionProps extends HTMLAttributes<HTMLDivElement> {
|
|
472
|
+
/** The accordion's items (`Accordion.Item`). */
|
|
473
|
+
children: ReactNode
|
|
474
|
+
/** Whether one or multiple items can be expanded. Defaults to `"single"`. */
|
|
475
|
+
type?: 'single' | 'multiple'
|
|
476
|
+
/** Allow collapsing all items when type is single. Defaults to `true` (Base UI's single mode is always collapsible). */
|
|
477
|
+
collapsible?: boolean
|
|
478
|
+
/** Initially expanded item value(s) — uncontrolled. A string for `type="single"`, an array for `type="multiple"`. */
|
|
479
|
+
defaultValue?: string | string[]
|
|
480
|
+
/** Controlled expanded value(s). Pair with `onValueChange`. */
|
|
481
|
+
value?: string | string[]
|
|
482
|
+
/** Callback when expanded state changes. Receives a string in single mode, an array in multiple mode. */
|
|
483
|
+
onValueChange?: ((value: string) => void) | ((value: string[]) => void)
|
|
484
|
+
/** What an open item tints: `item` holds a block across the row and its panel, `trigger` scopes it to the row and shows it on hover. Same prop on `Accordion.Group` and on a standalone `Accordion.Item`. Defaults to `"item"`. */
|
|
485
|
+
highlight?: 'trigger' | 'item'
|
|
486
|
+
/** Pins the accordion's rows to one step of the size ladder (default 36px, compact 28px). Omitted, they follow the surrounding SizeProvider. */
|
|
487
|
+
size?: SizeVariant
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
type AccordionComponent = ForwardRefExoticComponent<AccordionProps & RefAttributes<HTMLDivElement>> & {
|
|
491
|
+
Group: typeof AccordionGroup
|
|
492
|
+
Item: typeof AccordionItem
|
|
493
|
+
Trigger: typeof AccordionTrigger
|
|
494
|
+
Content: typeof AccordionContent
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Collapsible sections with animated expand/collapse and fluid hover in
|
|
499
|
+
* grouped mode.
|
|
500
|
+
*
|
|
501
|
+
* Accordion is the standalone form: every row carries its own hover fill.
|
|
502
|
+
* Accordion.Group takes the same props and adds one magnetic highlight that
|
|
503
|
+
* glides between rows, so give each item inside it an `index`. Built on Base
|
|
504
|
+
* UI Accordion (WAI-ARIA wiring, keyboard navigation, focus management).
|
|
505
|
+
* Uncontrolled through `defaultValue`, controlled through `value` +
|
|
506
|
+
* `onValueChange`.
|
|
507
|
+
*
|
|
508
|
+
* The panel grows out of its row (see Morph): a shape drips from where the
|
|
509
|
+
* row was pressed (its middle, from the keyboard), widens into the whole
|
|
510
|
+
* item, and the content is revealed by a clip from the same shape. Under the
|
|
511
|
+
* default `highlight="item"` that shape is the open tint, melted into the
|
|
512
|
+
* row through a liquid goo neck; closing folds it back into the row. The
|
|
513
|
+
* rows below ride the shape's bottom edge, so nothing jumps.
|
|
514
|
+
*
|
|
515
|
+
* Statics: Accordion.Group (the grouped root: type, collapsible,
|
|
516
|
+
* defaultValue, value, onValueChange, highlight, size), Accordion.Item (value,
|
|
517
|
+
* index, disabled), Accordion.Trigger (the row that toggles its item) and
|
|
518
|
+
* Accordion.Content (the collapsible panel: `effect` — `'goo'` or a plain
|
|
519
|
+
* `'morph'` — and the spring `tier`, `'slow'` or `'moderate'`).
|
|
520
|
+
*
|
|
521
|
+
* @example {@include ./examples.mdx}
|
|
522
|
+
*/
|
|
523
|
+
const Accordion = forwardRef<HTMLDivElement, AccordionProps>(
|
|
524
|
+
(
|
|
525
|
+
{
|
|
526
|
+
children,
|
|
527
|
+
type = 'single',
|
|
528
|
+
collapsible = true,
|
|
529
|
+
defaultValue,
|
|
530
|
+
value,
|
|
531
|
+
onValueChange,
|
|
532
|
+
highlight,
|
|
533
|
+
size,
|
|
534
|
+
className,
|
|
535
|
+
...props
|
|
536
|
+
},
|
|
537
|
+
ref,
|
|
538
|
+
) => {
|
|
539
|
+
void collapsible // Base UI's single-mode is always collapsible.
|
|
540
|
+
|
|
541
|
+
const [singleValue, setSingleValue] = useControllableState<string>(
|
|
542
|
+
type === 'single' ? (value as string | undefined) : undefined,
|
|
543
|
+
type === 'single' ? ((defaultValue as string | undefined) ?? '') : '',
|
|
544
|
+
type === 'single' ? (onValueChange as ((v: string) => void) | undefined) : undefined,
|
|
545
|
+
)
|
|
546
|
+
const [multipleValue, setMultipleValue] = useControllableState<string[]>(
|
|
547
|
+
type === 'multiple' ? (value as string[] | undefined) : undefined,
|
|
548
|
+
type === 'multiple' ? ((defaultValue as string[] | undefined) ?? []) : [],
|
|
549
|
+
type === 'multiple' ? (onValueChange as ((v: string[]) => void) | undefined) : undefined,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
const openValues = new Set<string>(type === 'multiple' ? multipleValue : singleValue ? [singleValue] : [])
|
|
553
|
+
|
|
554
|
+
const baseValue: string[] = [...openValues]
|
|
555
|
+
|
|
556
|
+
const baseOnValueChange = (next: string[]) => {
|
|
557
|
+
if (type === 'multiple') setMultipleValue(next)
|
|
558
|
+
else setSingleValue(next[0] ?? '')
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const root = (
|
|
562
|
+
<AccordionPrimitive.Root
|
|
563
|
+
value={baseValue}
|
|
564
|
+
onValueChange={baseOnValueChange}
|
|
565
|
+
multiple={type === 'multiple'}
|
|
566
|
+
render={rootProps => {
|
|
567
|
+
const { style: _s, ...restRoot } = rootProps as React.HTMLAttributes<HTMLDivElement>
|
|
568
|
+
return (
|
|
569
|
+
<div {...restRoot} ref={ref} className={cn('w-72 max-w-full flex flex-col', className)} {...props}>
|
|
570
|
+
<StandaloneOpenContext.Provider value={openValues}>
|
|
571
|
+
<StandaloneHighlightContext.Provider value={highlight}>{children}</StandaloneHighlightContext.Provider>
|
|
572
|
+
</StandaloneOpenContext.Provider>
|
|
573
|
+
</div>
|
|
574
|
+
)
|
|
575
|
+
}}
|
|
576
|
+
/>
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
// A size prop pins every row to one ladder step.
|
|
580
|
+
return size ? <SizeProvider size={size}>{root}</SizeProvider> : root
|
|
581
|
+
},
|
|
582
|
+
) as AccordionComponent
|
|
583
|
+
|
|
584
|
+
Accordion.displayName = 'Accordion'
|
|
585
|
+
|
|
586
|
+
const StandaloneOpenContext = createContext<Set<string>>(new Set())
|
|
587
|
+
|
|
588
|
+
const StandaloneHighlightContext = createContext<'trigger' | 'item' | undefined>(undefined)
|
|
589
|
+
|
|
590
|
+
// ─── AccordionItem ───────────────────────────────────────────────────────────
|
|
591
|
+
|
|
592
|
+
interface AccordionItemProps extends HTMLAttributes<HTMLDivElement> {
|
|
593
|
+
/** Unique identifier for this item. */
|
|
594
|
+
value: string
|
|
595
|
+
/** Position index for fluid hover. Required inside `Accordion.Group`, omit for standalone. */
|
|
596
|
+
index?: number
|
|
597
|
+
/** Whether this item is disabled. Defaults to `false`. */
|
|
598
|
+
disabled?: boolean
|
|
599
|
+
/** Standalone equivalent of the group's prop: what an open item tints. Ignored inside a group, which decides for all its rows. Defaults to the standalone root's `highlight`, else `"item"`. */
|
|
600
|
+
highlight?: 'trigger' | 'item'
|
|
601
|
+
/** The item's `Accordion.Trigger` and `Accordion.Content`. */
|
|
602
|
+
children: ReactNode
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const AccordionItem = forwardRef<HTMLDivElement, AccordionItemProps>(
|
|
606
|
+
({ value, index, disabled, highlight: highlightProp, children, className, ...props }, ref) => {
|
|
607
|
+
const internalRef = useRef<HTMLDivElement>(null)
|
|
608
|
+
const groupCtx = useAccordionGroup()
|
|
609
|
+
const standaloneOpen = useContext(StandaloneOpenContext)
|
|
610
|
+
const standaloneHighlight = useContext(StandaloneHighlightContext)
|
|
611
|
+
const highlight = groupCtx ? groupCtx.highlight : (highlightProp ?? standaloneHighlight ?? 'item')
|
|
612
|
+
|
|
613
|
+
const isOpen = groupCtx?.grouped ? groupCtx.openValues.has(value) : standaloneOpen.has(value)
|
|
614
|
+
|
|
615
|
+
const triggerRef = useRef<HTMLDivElement>(null)
|
|
616
|
+
const press = useRef<number | null>(null)
|
|
617
|
+
|
|
618
|
+
useRegisterFluidHoverItem(groupCtx?.grouped ? groupCtx.registerItem : undefined, index, triggerRef)
|
|
619
|
+
|
|
620
|
+
useEffect(() => {
|
|
621
|
+
if (groupCtx?.grouped && index !== undefined) {
|
|
622
|
+
if (isOpen) {
|
|
623
|
+
groupCtx.registerFullItem(index, internalRef.current)
|
|
624
|
+
} else {
|
|
625
|
+
groupCtx.registerFullItem(index, null)
|
|
626
|
+
}
|
|
627
|
+
return () => groupCtx.registerFullItem(index, null)
|
|
628
|
+
}
|
|
629
|
+
}, [index, groupCtx, isOpen])
|
|
630
|
+
|
|
631
|
+
return (
|
|
632
|
+
<AccordionItemContext.Provider value={{ index, value, isOpen, triggerRef, highlight, press }}>
|
|
633
|
+
<AccordionPrimitive.Item
|
|
634
|
+
// Through Base UI, which merges it into the render function's ref
|
|
635
|
+
// with the list-item registration that arrow-key navigation reads.
|
|
636
|
+
ref={node => {
|
|
637
|
+
;(internalRef as React.MutableRefObject<HTMLDivElement | null>).current = node
|
|
638
|
+
if (typeof ref === 'function') ref(node)
|
|
639
|
+
else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node
|
|
640
|
+
}}
|
|
641
|
+
value={value}
|
|
642
|
+
disabled={disabled}
|
|
643
|
+
render={itemProps => {
|
|
644
|
+
const { style: _s, ...restItem } = itemProps as React.HTMLAttributes<HTMLDivElement>
|
|
645
|
+
return (
|
|
646
|
+
<div
|
|
647
|
+
{...restItem}
|
|
648
|
+
data-fluid-hover-index={index}
|
|
649
|
+
// Positioned in a group too: the content's open tint covers
|
|
650
|
+
// the whole item (fluid hover adds positioned ancestors'
|
|
651
|
+
// offsets, so the row rects don't move).
|
|
652
|
+
className={cn('relative', className)}
|
|
653
|
+
{...props}
|
|
654
|
+
>
|
|
655
|
+
{children}
|
|
656
|
+
</div>
|
|
657
|
+
)
|
|
658
|
+
}}
|
|
659
|
+
/>
|
|
660
|
+
</AccordionItemContext.Provider>
|
|
661
|
+
)
|
|
662
|
+
},
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
AccordionItem.displayName = 'AccordionItem'
|
|
666
|
+
|
|
667
|
+
// ─── AccordionTrigger ────────────────────────────────────────────────────────
|
|
668
|
+
|
|
669
|
+
interface AccordionTriggerProps extends HTMLAttributes<HTMLButtonElement> {
|
|
670
|
+
/** Trigger label content. */
|
|
671
|
+
children: ReactNode
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const AccordionTrigger = forwardRef<HTMLButtonElement, AccordionTriggerProps>(
|
|
675
|
+
({ children, className, ...props }, ref) => {
|
|
676
|
+
const ChevronRight = useIcon('chevron-right')
|
|
677
|
+
const groupCtx = useAccordionGroup()
|
|
678
|
+
const { index, isOpen, triggerRef, highlight, press } = useAccordionItemContext()
|
|
679
|
+
const shape = useShape()
|
|
680
|
+
const sizeClasses = useSize()
|
|
681
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
682
|
+
|
|
683
|
+
const isActive = groupCtx?.grouped ? groupCtx.activeIndex === index : isHovered
|
|
684
|
+
|
|
685
|
+
// In layout px, so a scaled ancestor doesn't move the source.
|
|
686
|
+
const capture = (event: React.PointerEvent<HTMLDivElement>) => {
|
|
687
|
+
const row = event.currentTarget
|
|
688
|
+
const box = row.getBoundingClientRect()
|
|
689
|
+
press.current = ((event.clientX - box.left) * row.offsetWidth) / box.width
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const triggerContent = (
|
|
693
|
+
// Render Header as a <div>. Base UI's Header defaults to <h3>, which
|
|
694
|
+
// would be more semantic but breaks the ancestor selectors the styles
|
|
695
|
+
// rely on.
|
|
696
|
+
<AccordionPrimitive.Header render={<div />}>
|
|
697
|
+
<AccordionPrimitive.Trigger
|
|
698
|
+
ref={ref as React.Ref<HTMLElement>}
|
|
699
|
+
className={cn(
|
|
700
|
+
`relative z-10 flex items-center ${sizeClasses.gap} ${shape.item} ${sizeClasses.px} ${sizeClasses.variant === 'compact' ? 'py-1' : 'py-2'} w-full cursor-pointer outline-none select-none`,
|
|
701
|
+
!groupCtx?.grouped && 'focus-visible:ring-1 focus-visible:ring-focus-ring focus-visible:ring-offset-0',
|
|
702
|
+
className,
|
|
703
|
+
)}
|
|
704
|
+
{...(props as React.ButtonHTMLAttributes<HTMLButtonElement>)}
|
|
705
|
+
>
|
|
706
|
+
{/* Label with dual-layer text */}
|
|
707
|
+
<span className={cn('inline-grid flex-1 text-left', sizeClasses.text)}>
|
|
708
|
+
<span className="col-start-1 row-start-1 invisible weight-semibold" aria-hidden="true">
|
|
709
|
+
{children}
|
|
710
|
+
</span>
|
|
711
|
+
<span
|
|
712
|
+
className={cn(
|
|
713
|
+
'col-start-1 row-start-1 transition-[color,font-variation-settings] duration-fast',
|
|
714
|
+
isOpen || isActive ? 'text-foreground' : 'text-muted-foreground',
|
|
715
|
+
isOpen ? 'weight-semibold' : 'weight-normal',
|
|
716
|
+
)}
|
|
717
|
+
>
|
|
718
|
+
{children}
|
|
719
|
+
</span>
|
|
720
|
+
</span>
|
|
721
|
+
|
|
722
|
+
{/* Chevron */}
|
|
723
|
+
<motion.span
|
|
724
|
+
className="shrink-0 inline-flex items-center justify-center"
|
|
725
|
+
animate={{ rotate: isOpen ? 90 : 0 }}
|
|
726
|
+
transition={spring.fast}
|
|
727
|
+
>
|
|
728
|
+
<ChevronRight
|
|
729
|
+
size={sizeClasses.icon}
|
|
730
|
+
strokeWidth={isOpen || isActive ? 2 : 1.5}
|
|
731
|
+
className={cn(
|
|
732
|
+
'transition-[color,stroke-width] duration-fast',
|
|
733
|
+
isOpen || isActive ? 'text-foreground' : 'text-muted-foreground',
|
|
734
|
+
)}
|
|
735
|
+
/>
|
|
736
|
+
</motion.span>
|
|
737
|
+
</AccordionPrimitive.Trigger>
|
|
738
|
+
</AccordionPrimitive.Header>
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
if (groupCtx?.grouped) {
|
|
742
|
+
return (
|
|
743
|
+
<div ref={triggerRef} onPointerDown={capture}>
|
|
744
|
+
{triggerContent}
|
|
745
|
+
</div>
|
|
746
|
+
)
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return (
|
|
750
|
+
<div
|
|
751
|
+
className="relative"
|
|
752
|
+
onPointerDown={capture}
|
|
753
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
754
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
755
|
+
>
|
|
756
|
+
{/* Open tint, scoped to this row: the panel below keeps the page's
|
|
757
|
+
own surface, the way a sidebar row highlights without colouring
|
|
758
|
+
its sub-tree. */}
|
|
759
|
+
<AnimatePresence>
|
|
760
|
+
{isOpen && highlight === 'trigger' && isHovered && (
|
|
761
|
+
<motion.div
|
|
762
|
+
className={`absolute inset-0 ${shape.bg} bg-accent/20 dark:bg-accent/12 pointer-events-none`}
|
|
763
|
+
initial={{ opacity: 0 }}
|
|
764
|
+
animate={{ opacity: 1 }}
|
|
765
|
+
// The expanded tint rides the moderate tier, like the grouped
|
|
766
|
+
// one — it marks a state, where the hover fill below tracks the
|
|
767
|
+
// pointer and stays fast.
|
|
768
|
+
exit={{ opacity: 0, transition: spring.moderate.exit }}
|
|
769
|
+
transition={{ duration: spring.moderate.exit.duration }}
|
|
770
|
+
/>
|
|
771
|
+
)}
|
|
772
|
+
</AnimatePresence>
|
|
773
|
+
<AnimatePresence>
|
|
774
|
+
{isHovered && (
|
|
775
|
+
<motion.div
|
|
776
|
+
className={`absolute inset-0 z-1 ${shape.bg} bg-hover pointer-events-none`}
|
|
777
|
+
initial={{ opacity: 0 }}
|
|
778
|
+
animate={{ opacity: 1 }}
|
|
779
|
+
exit={{ opacity: 0, transition: spring.fast.exit }}
|
|
780
|
+
transition={{ duration: spring.fast.duration }}
|
|
781
|
+
/>
|
|
782
|
+
)}
|
|
783
|
+
</AnimatePresence>
|
|
784
|
+
{triggerContent}
|
|
785
|
+
</div>
|
|
786
|
+
)
|
|
787
|
+
},
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
AccordionTrigger.displayName = 'AccordionTrigger'
|
|
791
|
+
|
|
792
|
+
// ─── AccordionContent ────────────────────────────────────────────────────────
|
|
793
|
+
|
|
794
|
+
interface AccordionContentProps extends HTMLAttributes<HTMLDivElement> {
|
|
795
|
+
/** Collapsible content. */
|
|
796
|
+
children: ReactNode
|
|
797
|
+
/** How the panel grows out of its row (see Morph): `goo` melts the open tint out of the row through a liquid neck, `morph` grows it without one. Defaults to `'goo'`. */
|
|
798
|
+
effect?: 'goo' | 'morph'
|
|
799
|
+
/** Spring tier the panel opens on; it closes on the tier's faster exit tween. Defaults to `'slow'`. */
|
|
800
|
+
tier?: 'moderate' | 'slow'
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
interface Rect {
|
|
804
|
+
x: number
|
|
805
|
+
y: number
|
|
806
|
+
w: number
|
|
807
|
+
h: number
|
|
808
|
+
r: number
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// The morph engine's rect interpolation (lib/use-morph.ts). `t` stops at 1:
|
|
812
|
+
// the slow tier's bounce would push the rows below past their place and back.
|
|
813
|
+
function grow(a: Rect, b: Rect, t: number): Rect {
|
|
814
|
+
const p = Math.min(1, t)
|
|
815
|
+
const at = (from: number, to: number) => from + (to - from) * p
|
|
816
|
+
const w = at(a.w, b.w)
|
|
817
|
+
const h = at(a.h, b.h)
|
|
818
|
+
return { x: at(a.x, b.x), y: at(a.y, b.y), w, h, r: Math.min(at(a.r, b.r), w / 2, h / 2) }
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function place(el: HTMLElement | null, { x, y, w, h, r }: Rect) {
|
|
822
|
+
if (!el) return
|
|
823
|
+
el.style.left = `${x}px`
|
|
824
|
+
el.style.top = `${y}px`
|
|
825
|
+
el.style.width = `${w}px`
|
|
826
|
+
el.style.height = `${h}px`
|
|
827
|
+
el.style.borderRadius = `${r}px`
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const AccordionContent = forwardRef<HTMLDivElement, AccordionContentProps>(
|
|
831
|
+
({ children, className, effect = 'goo', tier = 'slow', ...props }, ref) => {
|
|
832
|
+
const groupCtx = useAccordionGroup()
|
|
833
|
+
const { isOpen, highlight, press } = useAccordionItemContext()
|
|
834
|
+
const shape = useShape()
|
|
835
|
+
const sizeClasses = useSize()
|
|
836
|
+
const reduceMotion = useReduceMotion()
|
|
837
|
+
const gooId = `accordion-goo-${useId().replace(/:/g, '')}`
|
|
838
|
+
const progress = useMotionValue(isOpen ? 1 : 0)
|
|
839
|
+
// The tint is there for most of the morph and fades only as the shape
|
|
840
|
+
// gets back into the row, so the shape, not a fade, carries the motion.
|
|
841
|
+
const tint = useTransform(progress, [0, 0.3], [0, 1])
|
|
842
|
+
const tinted = highlight === 'item'
|
|
843
|
+
const remeasure = useRef(groupCtx?.remeasure)
|
|
844
|
+
remeasure.current = groupCtx?.remeasure
|
|
845
|
+
|
|
846
|
+
const refs = {
|
|
847
|
+
panel: useRef<HTMLDivElement>(null),
|
|
848
|
+
content: useRef<HTMLDivElement>(null),
|
|
849
|
+
rest: useRef<HTMLDivElement>(null),
|
|
850
|
+
goo: useRef<HTMLDivElement>(null),
|
|
851
|
+
row: useRef<HTMLDivElement>(null),
|
|
852
|
+
shape: useRef<HTMLDivElement>(null),
|
|
853
|
+
}
|
|
854
|
+
useImperativeHandle(ref, () => refs.panel.current as HTMLDivElement, [])
|
|
855
|
+
|
|
856
|
+
// In the item's own coordinates: `x` is the source's left edge, `row`
|
|
857
|
+
// the trigger row's height (the panel's offsetTop), `body` the content's.
|
|
858
|
+
const geometry = useRef({ x: 0, width: 0, row: 0, body: 0 })
|
|
859
|
+
|
|
860
|
+
// One frame of the morph. The shape grows from a point in the middle of
|
|
861
|
+
// the row to the whole item; the panel's layout height is the shape's
|
|
862
|
+
// reach below the row, so the rows underneath move with its bottom edge,
|
|
863
|
+
// and the content shows only inside the shape.
|
|
864
|
+
const render = (p: number) => {
|
|
865
|
+
const { x, width, row, body } = geometry.current
|
|
866
|
+
const rect = grow(
|
|
867
|
+
{ x, y: row / 2, w: 0, h: 0, r: width },
|
|
868
|
+
{ x: 0, y: 0, w: width, h: row + body, r: shape.bgRadius },
|
|
869
|
+
p,
|
|
870
|
+
)
|
|
871
|
+
const reach = rect.y + rect.h - row
|
|
872
|
+
const { panel, content } = refs
|
|
873
|
+
if (panel.current) panel.current.style.height = `${Math.max(0, reach)}px`
|
|
874
|
+
if (content.current) {
|
|
875
|
+
content.current.style.clipPath = `inset(${rect.y - row}px ${width - rect.x - rect.w}px ${body - reach}px ${rect.x}px round ${rect.r}px)`
|
|
876
|
+
}
|
|
877
|
+
place(refs.shape.current, rect)
|
|
878
|
+
remeasure.current?.()
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Base UI's Panel would apply `hidden` the moment a controlled item
|
|
882
|
+
// closes (no CSS animation for it to wait on) — `display: none` would
|
|
883
|
+
// cut the morph off mid-flight. So the `hidden` attribute is ours, and
|
|
884
|
+
// only lands once the close has finished.
|
|
885
|
+
const [exitComplete, setExitComplete] = useState(!isOpen)
|
|
886
|
+
if (isOpen && exitComplete) {
|
|
887
|
+
// Reset during render so the panel is laid out before the opening
|
|
888
|
+
// morph's first frame.
|
|
889
|
+
setExitComplete(false)
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Back to rest: open, the panel's height and the tint are their classes
|
|
893
|
+
// again (auto height, one block over the item), so they follow content
|
|
894
|
+
// that resizes; closed, the panel goes `hidden`.
|
|
895
|
+
const settle = (open: boolean) => {
|
|
896
|
+
const { panel, content, goo, rest } = refs
|
|
897
|
+
if (panel.current) panel.current.style.height = open ? '' : '0px'
|
|
898
|
+
// Closed, the content stays clipped shut: `hidden` lands with React's
|
|
899
|
+
// next commit, a task later, and unclipped content would paint below
|
|
900
|
+
// the collapsed row for a frame in between.
|
|
901
|
+
if (content.current) content.current.style.clipPath = open ? '' : 'inset(50%)'
|
|
902
|
+
if (goo.current) goo.current.style.display = ''
|
|
903
|
+
if (rest.current) rest.current.style.display = ''
|
|
904
|
+
remeasure.current?.()
|
|
905
|
+
if (!open) setExitComplete(true)
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const wasOpen = useRef(isOpen)
|
|
909
|
+
useIsoLayoutEffect(() => {
|
|
910
|
+
if (wasOpen.current === isOpen) return
|
|
911
|
+
wasOpen.current = isOpen
|
|
912
|
+
const { panel, content, goo, rest, row } = refs
|
|
913
|
+
if (!panel.current || !content.current) return
|
|
914
|
+
const g = geometry.current
|
|
915
|
+
g.width = panel.current.offsetWidth
|
|
916
|
+
g.row = panel.current.offsetTop
|
|
917
|
+
g.body = content.current.offsetHeight
|
|
918
|
+
// A reopen mid-close keeps its source, so the shape never jumps.
|
|
919
|
+
if (isOpen && progress.get() === 0) g.x = press.current ?? g.width / 2
|
|
920
|
+
press.current = null
|
|
921
|
+
|
|
922
|
+
if (reduceMotion) {
|
|
923
|
+
progress.set(isOpen ? 1 : 0)
|
|
924
|
+
settle(isOpen)
|
|
925
|
+
return
|
|
926
|
+
}
|
|
927
|
+
if (goo.current) goo.current.style.display = 'block'
|
|
928
|
+
if (rest.current) rest.current.style.display = 'none'
|
|
929
|
+
if (row.current) row.current.style.height = `${g.row}px`
|
|
930
|
+
render(progress.get())
|
|
931
|
+
const { exit, ...enter } = spring[tier]
|
|
932
|
+
const controls = animate(progress, isOpen ? 1 : 0, {
|
|
933
|
+
...(isOpen ? enter : exit),
|
|
934
|
+
onUpdate: render,
|
|
935
|
+
onComplete: () => settle(isOpen),
|
|
936
|
+
})
|
|
937
|
+
return () => controls.stop()
|
|
938
|
+
}, [isOpen])
|
|
939
|
+
|
|
940
|
+
// The content keeps its natural size while the panel morphs, so this
|
|
941
|
+
// never feeds back: it retargets a running morph and, at rest, lets the
|
|
942
|
+
// group re-measure rows a nested accordion pushed around.
|
|
943
|
+
useIsoLayoutEffect(() => {
|
|
944
|
+
const content = refs.content.current
|
|
945
|
+
if (!content) return
|
|
946
|
+
const observer = new ResizeObserver(() => {
|
|
947
|
+
if (content.offsetHeight > 0) geometry.current.body = content.offsetHeight
|
|
948
|
+
remeasure.current?.()
|
|
949
|
+
})
|
|
950
|
+
observer.observe(content)
|
|
951
|
+
return () => observer.disconnect()
|
|
952
|
+
}, [])
|
|
953
|
+
|
|
954
|
+
// Render through `<AccordionPrimitive.Panel keepMounted>` so the panel
|
|
955
|
+
// element persists through the close and the trigger ↔ panel ARIA
|
|
956
|
+
// contract stays intact: the panel carries `role="region"`,
|
|
957
|
+
// `aria-labelledby` and the id that the Trigger's `aria-controls` points
|
|
958
|
+
// to.
|
|
959
|
+
return (
|
|
960
|
+
<AccordionPrimitive.Panel
|
|
961
|
+
keepMounted
|
|
962
|
+
render={panelProps => {
|
|
963
|
+
const {
|
|
964
|
+
hidden: _baseHidden,
|
|
965
|
+
// Only carries the --accordion-panel-height/width vars, which
|
|
966
|
+
// stay 'auto' since Base UI never measures JS-driven animations.
|
|
967
|
+
style: _baseStyle,
|
|
968
|
+
...restPanel
|
|
969
|
+
} = panelProps as React.HTMLAttributes<HTMLDivElement> & {
|
|
970
|
+
hidden?: boolean
|
|
971
|
+
}
|
|
972
|
+
return (
|
|
973
|
+
<div {...restPanel} hidden={!isOpen && exitComplete}>
|
|
974
|
+
{/* Neither this nor the panel is positioned, so the tint
|
|
975
|
+
below covers the whole item (row + panel). */}
|
|
976
|
+
<div ref={refs.panel} className={className} {...props}>
|
|
977
|
+
{tinted && (
|
|
978
|
+
<motion.div
|
|
979
|
+
aria-hidden
|
|
980
|
+
className="pointer-events-none absolute inset-0"
|
|
981
|
+
initial={false}
|
|
982
|
+
animate={{ opacity: groupCtx?.dimOpen ? 0.7 : 1 }}
|
|
983
|
+
transition={{ duration: spring.moderate.exit.duration }}
|
|
984
|
+
>
|
|
985
|
+
<motion.div className="absolute inset-0" style={{ opacity: tint }}>
|
|
986
|
+
{/* The tint's strength: solid shapes, so the goo
|
|
987
|
+
threshold sees full alpha, faded as one layer. */}
|
|
988
|
+
<div className="absolute inset-0 opacity-20 dark:opacity-12">
|
|
989
|
+
<div ref={refs.rest} className={cn('absolute inset-0 bg-accent', shape.bg)} />
|
|
990
|
+
{effect === 'goo' && <GooFilter id={gooId} blur={shape.bgRadius * GOO_BLUR_RATIO} />}
|
|
991
|
+
{/* The filter is the effect itself: it melts the row and the growing shape into one. */}
|
|
992
|
+
<div
|
|
993
|
+
ref={refs.goo}
|
|
994
|
+
className="absolute inset-0 hidden"
|
|
995
|
+
style={effect === 'goo' ? { filter: `url(#${gooId})` } : undefined}
|
|
996
|
+
>
|
|
997
|
+
<div ref={refs.row} className={cn('absolute inset-x-0 top-0 bg-accent', shape.bg)} />
|
|
998
|
+
<div ref={refs.shape} className="absolute bg-accent" />
|
|
999
|
+
</div>
|
|
1000
|
+
</div>
|
|
1001
|
+
</motion.div>
|
|
1002
|
+
</motion.div>
|
|
1003
|
+
)}
|
|
1004
|
+
<div
|
|
1005
|
+
ref={refs.content}
|
|
1006
|
+
className={cn(
|
|
1007
|
+
'relative pt-1 text-muted-foreground',
|
|
1008
|
+
sizeClasses.px,
|
|
1009
|
+
sizeClasses.text,
|
|
1010
|
+
sizeClasses.variant === 'compact' ? 'pb-2.5' : 'pb-3',
|
|
1011
|
+
)}
|
|
1012
|
+
>
|
|
1013
|
+
{children}
|
|
1014
|
+
</div>
|
|
1015
|
+
</div>
|
|
1016
|
+
</div>
|
|
1017
|
+
)
|
|
1018
|
+
}}
|
|
1019
|
+
/>
|
|
1020
|
+
)
|
|
1021
|
+
},
|
|
1022
|
+
)
|
|
1023
|
+
|
|
1024
|
+
AccordionContent.displayName = 'AccordionContent'
|
|
1025
|
+
|
|
1026
|
+
Object.assign(Accordion, {
|
|
1027
|
+
Group: AccordionGroup,
|
|
1028
|
+
Item: AccordionItem,
|
|
1029
|
+
Trigger: AccordionTrigger,
|
|
1030
|
+
Content: AccordionContent,
|
|
1031
|
+
})
|
|
1032
|
+
|
|
1033
|
+
export { Accordion, AccordionContent, AccordionGroup, AccordionItem, AccordionTrigger }
|
|
1034
|
+
export default Accordion
|