@justanarthur/modo-ui 0.6.0-canary.8 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -164
- package/dist/base.css +4 -141
- package/dist/blocks/sidebar-app/index.js +26 -40
- package/dist/components/AI/input-message/index.js +16 -23
- package/dist/components/AI/thinking-indicator/index.js +6 -9
- package/dist/components/accordion/index.d.ts +11 -39
- package/dist/components/accordion/index.js +13 -18
- package/dist/components/badge/index.d.ts +11 -18
- package/dist/components/badge/index.js +7 -12
- package/dist/components/button/index.d.ts +17 -53
- package/dist/components/button/index.js +6 -11
- package/dist/components/card/index.d.ts +9 -32
- package/dist/components/card/index.js +11 -17
- package/dist/components/copy/index.d.ts +8 -41
- package/dist/components/copy/index.js +9 -15
- package/dist/components/inputs/checkbox/index.d.ts +9 -28
- package/dist/components/inputs/checkbox/index.js +13 -18
- package/dist/components/inputs/color-picker/index.d.ts +14 -22
- package/dist/components/inputs/color-picker/index.js +18 -26
- package/dist/components/inputs/combobox/index.d.ts +20 -85
- package/dist/components/inputs/combobox/index.js +18 -27
- package/dist/components/inputs/date-time/calendar/index.d.ts +18 -24
- package/dist/components/inputs/date-time/calendar/index.js +11 -15
- package/dist/components/inputs/date-time/date-picker/index.d.ts +16 -105
- package/dist/components/inputs/date-time/date-picker/index.js +25 -35
- package/dist/components/inputs/date-time/date-range-picker/index.d.ts +12 -106
- package/dist/components/inputs/date-time/date-range-picker/index.js +24 -35
- package/dist/components/inputs/date-time/duration-picker/index.d.ts +13 -66
- package/dist/components/inputs/date-time/duration-picker/index.js +24 -33
- package/dist/components/inputs/date-time/month-picker/index.d.ts +10 -60
- package/dist/components/inputs/date-time/month-picker/index.js +23 -32
- package/dist/components/inputs/date-time/time-picker/index.d.ts +10 -58
- package/dist/components/inputs/date-time/time-picker/index.js +24 -33
- package/dist/components/inputs/input/index.d.ts +13 -95
- package/dist/components/inputs/input/index.js +9 -18
- package/dist/components/inputs/radio/index.d.ts +5 -20
- package/dist/components/inputs/radio/index.js +12 -16
- package/dist/components/inputs/search/index.d.ts +3 -3
- package/dist/components/inputs/search/index.js +18 -26
- package/dist/components/inputs/select/index.d.ts +23 -99
- package/dist/components/inputs/select/index.js +14 -29
- package/dist/components/inputs/slider/index.d.ts +0 -9
- package/dist/components/inputs/slider/index.js +5 -8
- package/dist/components/inputs/switch/index.d.ts +2 -8
- package/dist/components/inputs/switch/index.js +12 -15
- package/dist/components/inputs/textarea/index.d.ts +1 -20
- package/dist/components/inputs/textarea/index.js +6 -9
- package/dist/components/navigation/sidebar/index.d.ts +21 -44
- package/dist/components/navigation/sidebar/index.js +31 -42
- package/dist/components/navigation/tabs/index.d.ts +2 -8
- package/dist/components/navigation/tabs/index.js +10 -15
- package/dist/components/navigation/tabs-subtle/index.js +10 -13
- package/dist/components/overlays/alert-dialog/index.d.ts +16 -135
- package/dist/components/overlays/alert-dialog/index.js +13 -23
- package/dist/components/overlays/command/index.d.ts +20 -25
- package/dist/components/overlays/command/index.js +17 -25
- package/dist/components/overlays/context-menu/index.d.ts +17 -39
- package/dist/components/overlays/context-menu/index.js +18 -29
- package/dist/components/overlays/dialog/index.d.ts +16 -42
- package/dist/components/overlays/dialog/index.js +12 -20
- package/dist/components/overlays/dropdown/index.d.ts +34 -304
- package/dist/components/overlays/dropdown/index.js +17 -44
- package/dist/components/overlays/popover/index.d.ts +12 -38
- package/dist/components/overlays/popover/index.js +9 -13
- package/dist/components/overlays/preview-card/index.js +8 -12
- package/dist/components/overlays/sheet/index.d.ts +8 -25
- package/dist/components/overlays/sheet/index.js +13 -19
- package/dist/components/overlays/toast/index.d.ts +50 -209
- package/dist/components/overlays/toast/index.js +10 -23
- package/dist/components/overlays/tooltip/index.d.ts +21 -123
- package/dist/components/overlays/tooltip/index.js +6 -16
- package/dist/components/table/index.d.ts +8 -63
- package/dist/components/table/index.js +9 -20
- package/dist/index.d.ts +1048 -2999
- package/dist/index.js +82 -231
- package/dist/preset.d.ts +0 -22
- package/dist/preset.js +2 -24
- package/dist/primitives/code/index.js +5 -8
- package/dist/primitives/craft/index.js +6 -10
- package/dist/primitives/fluid-hover/index.js +8 -10
- package/dist/primitives/morph/index.js +10 -16
- package/dist/primitives/motion/index.js +8 -11
- package/dist/primitives/scroll-area/index.js +3 -5
- package/dist/primitives/sizes/index.js +4 -6
- package/dist/primitives/surface/index.js +4 -6
- package/dist/shared/{components-4y816hwp.js → components-0q0sqfh4.js} +49 -27
- package/dist/shared/components-0sn7ypjw.js +61 -0
- package/dist/shared/{components-vc864c51.js → components-11da9ajg.js} +5 -8
- package/dist/shared/components-1434p1mh.js +123 -0
- package/dist/shared/{components-t2a3qmy9.js → components-1z6tdadk.js} +24 -40
- package/dist/shared/{components-qs4rkqsj.js → components-27nnvrwe.js} +14 -15
- package/dist/shared/{components-av5t26rc.js → components-2dxkv6zm.js} +7 -8
- package/dist/shared/{components-wqrvqq5p.js → components-2ecg8svx.js} +152 -234
- package/dist/shared/{components-3ydr2fqe.js → components-2ph6t61s.js} +34 -37
- package/dist/shared/{components-6gk118w3.js → components-39jq9va1.js} +24 -29
- package/dist/shared/components-3ky44908.js +9 -0
- package/dist/shared/{components-y1qvbawa.js → components-3mkepka5.js} +25 -28
- package/dist/shared/{components-xqszvkj4.js → components-4700p9kz.js} +22 -24
- package/dist/shared/{components-yhgq0qxx.js → components-59efxjd9.js} +12 -13
- package/dist/shared/{components-9j9abkct.js → components-5vgpnxgy.js} +27 -51
- package/dist/shared/{components-8tt3xbb5.js → components-5xc2j8jz.js} +18 -19
- package/dist/shared/{components-jymmaevv.js → components-6p8tfcd6.js} +29 -57
- package/dist/shared/{components-4nck8acq.js → components-7r3etddy.js} +54 -19
- package/dist/shared/{components-skfd7kff.js → components-8snj490h.js} +32 -78
- package/dist/shared/{components-ctqmge8t.js → components-b0eac83j.js} +30 -9
- package/dist/shared/{components-9sttq78t.js → components-b2wyq2cq.js} +59 -41
- package/dist/shared/{components-vkwq2k96.js → components-b3c5et5b.js} +38 -49
- package/dist/shared/{components-rrbpmfx4.js → components-c6s6b43g.js} +2 -3
- package/dist/shared/{components-pw06rbxq.js → components-chacnk61.js} +19 -19
- package/dist/shared/{components-71nbt03m.js → components-cv16mryv.js} +7 -26
- package/dist/shared/{components-eqze3y30.js → components-d6xtjht3.js} +30 -35
- package/dist/shared/{components-5y5shqfd.js → components-dk0gjr5a.js} +18 -19
- package/dist/shared/{components-khspp26b.js → components-fzrq6tr3.js} +34 -37
- package/dist/shared/{components-km7fhgbk.js → components-gc5vtq2f.js} +30 -66
- package/dist/shared/components-gf5s6ap9.js +42 -0
- package/dist/shared/{components-qqcmtrbv.js → components-gz8zf3ec.js} +55 -56
- package/dist/shared/{components-mattt5jp.js → components-h2ajmhwn.js} +35 -107
- package/dist/shared/{components-awra5sff.js → components-hqeq3w9q.js} +16 -24
- package/dist/shared/{components-5tzhqbcw.js → components-hw9yk32z.js} +24 -36
- package/dist/shared/components-hyq23phv.js +310 -0
- package/dist/shared/{components-8mt5bsv6.js → components-jbbmdr74.js} +22 -41
- package/dist/shared/{components-dcc02hs2.js → components-jjw69c4c.js} +4 -7
- package/dist/shared/{components-2syq4mwc.js → components-jthe1pwb.js} +420 -139
- package/dist/shared/{components-r7q1zm7r.js → components-jwst3vtj.js} +76 -224
- package/dist/shared/{components-qdm3yh26.js → components-kvr7f9hy.js} +10 -19
- package/dist/shared/{components-s8yqk2dc.js → components-kwa6mrf9.js} +11 -46
- package/dist/shared/{components-de99ykt7.js → components-men63rwj.js} +2 -3
- package/dist/shared/{components-6b0zb8z3.js → components-mjthm1kf.js} +9 -13
- package/dist/shared/{components-2c64r8c9.js → components-n42c79bz.js} +22 -23
- package/dist/shared/{components-tz1fv35x.js → components-n7mjr357.js} +2 -3
- package/dist/shared/components-ng72k97e.js +128 -0
- package/dist/shared/{components-dyae0y7h.js → components-pbmqctfa.js} +65 -169
- package/dist/shared/{components-px2zm7dc.js → components-q5xj5c16.js} +8 -9
- package/dist/shared/components-raknhppb.js +64 -0
- package/dist/shared/{components-8rb25gbe.js → components-rr7a6ja9.js} +27 -59
- package/dist/shared/{components-5012r6gr.js → components-s083f38z.js} +34 -164
- package/dist/shared/components-s5ngg32c.js +267 -0
- package/dist/shared/components-sn1rxx1d.js +143 -0
- package/dist/shared/{components-r8eqs7gz.js → components-ta7shw28.js} +4 -4
- package/dist/shared/{components-9as3rf68.js → components-vc7vq5nz.js} +3 -6
- package/dist/shared/{components-ft3zdc79.js → components-vfbhxf5r.js} +5 -6
- package/dist/shared/components-vynjtrk6.js +19 -0
- package/dist/shared/{components-w0xq6q9k.js → components-w1r67ag9.js} +149 -294
- package/dist/shared/{components-z0bqtvcp.js → components-w90endve.js} +72 -71
- package/dist/shared/{components-t5pk5nmc.js → components-wvp1m52g.js} +102 -149
- package/dist/shared/components-wy9jpqny.js +146 -0
- package/dist/shared/{components-5sxjeh6h.js → components-y9y0g8er.js} +28 -52
- package/dist/shared/components-ypp01w3p.js +986 -0
- package/dist/shared/{components-t50hrswb.js → components-zfnmn5d4.js} +36 -82
- package/dist/shared/{components-jqpefjsn.js → components-zrkzcayz.js} +6 -54
- package/dist/shared/{components-vf45j12c.js → components-zx6b6yby.js} +18 -4
- package/dist/styles.css +32 -320
- package/dist/tailwind.css +0 -16
- package/dist/tokens.css +0 -67
- package/package.json +4 -227
- package/DESIGN.md +0 -623
- package/blocks/sidebar-app/examples.mdx +0 -33
- package/blocks/sidebar-app/index.tsx +0 -218
- package/blocks/sidebar-app/inset-topbar.tsx +0 -25
- package/blocks/sidebar-app/menu-grid.ts +0 -26
- package/blocks/sidebar-app/nav-data.ts +0 -36
- package/blocks/sidebar-app/search-field.tsx +0 -48
- package/blocks/sidebar-app/user-footer.tsx +0 -67
- package/blocks/sidebar-app/workspace-header.tsx +0 -129
- package/components/AI/input-message/examples.mdx +0 -130
- package/components/AI/input-message/file-thumbnail.tsx +0 -173
- package/components/AI/input-message/index.tsx +0 -1385
- package/components/AI/thinking-indicator/examples.mdx +0 -27
- package/components/AI/thinking-indicator/index.tsx +0 -210
- package/components/accordion/examples.mdx +0 -233
- package/components/accordion/index.tsx +0 -1071
- package/components/avatar/examples.mdx +0 -95
- package/components/avatar/index.tsx +0 -129
- package/components/badge/change-demo.tsx +0 -29
- package/components/badge/examples.mdx +0 -124
- package/components/badge/index.tsx +0 -225
- package/components/badge/variants.ts +0 -73
- package/components/button/examples.mdx +0 -168
- package/components/button/index.tsx +0 -424
- package/components/button/loading-demo.tsx +0 -25
- package/components/button/variants.ts +0 -168
- package/components/card/examples.mdx +0 -251
- package/components/card/index.tsx +0 -1070
- package/components/copy/examples.mdx +0 -125
- package/components/copy/index.tsx +0 -662
- package/components/inputs/checkbox/examples.mdx +0 -87
- package/components/inputs/checkbox/index.tsx +0 -611
- package/components/inputs/checkbox/select-all-demo.tsx +0 -48
- package/components/inputs/color-picker/examples.mdx +0 -111
- package/components/inputs/color-picker/index.tsx +0 -1981
- package/components/inputs/color-picker/variants.ts +0 -401
- package/components/inputs/combobox/examples.mdx +0 -366
- package/components/inputs/combobox/index.tsx +0 -1527
- package/components/inputs/combobox/remote-demo.tsx +0 -105
- package/components/inputs/date-time/calendar/examples.mdx +0 -45
- package/components/inputs/date-time/calendar/index.tsx +0 -370
- package/components/inputs/date-time/calendar/use-month-window.ts +0 -173
- package/components/inputs/date-time/date-picker/examples.mdx +0 -243
- package/components/inputs/date-time/date-picker/index.tsx +0 -476
- package/components/inputs/date-time/date-picker/masked-date-input.tsx +0 -348
- package/components/inputs/date-time/date-range-picker/examples.mdx +0 -93
- package/components/inputs/date-time/date-range-picker/index.tsx +0 -525
- package/components/inputs/date-time/duration-picker/examples.mdx +0 -40
- package/components/inputs/date-time/duration-picker/index.tsx +0 -253
- package/components/inputs/date-time/month-picker/examples.mdx +0 -45
- package/components/inputs/date-time/month-picker/index.tsx +0 -254
- package/components/inputs/date-time/picker-shell.tsx +0 -581
- package/components/inputs/date-time/time-columns.tsx +0 -94
- package/components/inputs/date-time/time-picker/examples.mdx +0 -57
- package/components/inputs/date-time/time-picker/index.tsx +0 -227
- package/components/inputs/field/examples.mdx +0 -117
- package/components/inputs/field/index.tsx +0 -268
- package/components/inputs/file-dropzone/examples.mdx +0 -83
- package/components/inputs/file-dropzone/file-row.tsx +0 -201
- package/components/inputs/file-dropzone/index.tsx +0 -316
- package/components/inputs/file-dropzone/upload-demo.tsx +0 -67
- package/components/inputs/file-dropzone/variants.ts +0 -94
- package/components/inputs/input/examples.mdx +0 -166
- package/components/inputs/input/index.tsx +0 -494
- package/components/inputs/input/password-demo.tsx +0 -30
- package/components/inputs/label/examples.mdx +0 -67
- package/components/inputs/label/index.tsx +0 -85
- package/components/inputs/number-input/examples.mdx +0 -69
- package/components/inputs/number-input/index.tsx +0 -221
- package/components/inputs/radio/examples.mdx +0 -77
- package/components/inputs/radio/index.tsx +0 -636
- package/components/inputs/search/examples.mdx +0 -74
- package/components/inputs/search/index.tsx +0 -333
- package/components/inputs/select/examples.mdx +0 -296
- package/components/inputs/select/index.tsx +0 -1044
- package/components/inputs/select/variants.ts +0 -32
- package/components/inputs/slider/examples.mdx +0 -153
- package/components/inputs/slider/index.tsx +0 -1867
- package/components/inputs/switch/examples.mdx +0 -96
- package/components/inputs/switch/index.tsx +0 -504
- package/components/inputs/textarea/examples.mdx +0 -126
- package/components/inputs/textarea/index.tsx +0 -207
- package/components/navigation/navigation-menu/examples.mdx +0 -136
- package/components/navigation/navigation-menu/index.tsx +0 -704
- package/components/navigation/pagination/examples.mdx +0 -74
- package/components/navigation/pagination/index.tsx +0 -401
- package/components/navigation/sidebar/examples.mdx +0 -325
- package/components/navigation/sidebar/index.tsx +0 -453
- package/components/navigation/sidebar/sidebar-core.tsx +0 -1408
- package/components/navigation/sidebar/sidebar-menu.tsx +0 -1495
- package/components/navigation/sidebar/sidebar.css +0 -74
- package/components/navigation/sidebar/variants.ts +0 -55
- package/components/navigation/tabs/examples.mdx +0 -89
- package/components/navigation/tabs/index.tsx +0 -494
- package/components/navigation/tabs-subtle/examples.mdx +0 -49
- package/components/navigation/tabs-subtle/index.tsx +0 -512
- package/components/overlays/alert-dialog/examples.mdx +0 -123
- package/components/overlays/alert-dialog/index.tsx +0 -276
- package/components/overlays/alert-dialog/pending-demo.tsx +0 -36
- package/components/overlays/command/examples.mdx +0 -113
- package/components/overlays/command/index.tsx +0 -425
- package/components/overlays/context-menu/examples.mdx +0 -56
- package/components/overlays/context-menu/index.tsx +0 -279
- package/components/overlays/dialog/examples.mdx +0 -165
- package/components/overlays/dialog/index.tsx +0 -451
- package/components/overlays/dropdown/dropdown-search.tsx +0 -395
- package/components/overlays/dropdown/examples.mdx +0 -444
- package/components/overlays/dropdown/index.tsx +0 -1241
- package/components/overlays/dropdown/menu-item.tsx +0 -510
- package/components/overlays/dropdown/menu-parts.tsx +0 -725
- package/components/overlays/popover/anchored-demo.tsx +0 -59
- package/components/overlays/popover/examples.mdx +0 -203
- package/components/overlays/popover/index.tsx +0 -317
- package/components/overlays/preview-card/examples.mdx +0 -61
- package/components/overlays/preview-card/index.tsx +0 -204
- package/components/overlays/sheet/examples.mdx +0 -101
- package/components/overlays/sheet/index.tsx +0 -307
- package/components/overlays/toast/examples.mdx +0 -85
- package/components/overlays/toast/global-demo.tsx +0 -72
- package/components/overlays/toast/index.tsx +0 -544
- package/components/overlays/toast/manager.ts +0 -193
- package/components/overlays/tooltip/examples.mdx +0 -106
- package/components/overlays/tooltip/index.tsx +0 -539
- package/components/progress/examples.mdx +0 -59
- package/components/progress/index.tsx +0 -174
- package/components/progress/progress.css +0 -49
- package/components/progress/step-demo.tsx +0 -27
- package/components/table/examples.mdx +0 -188
- package/components/table/index.tsx +0 -535
- package/components/table/sort-demo.tsx +0 -61
- package/dist/components/avatar/index.d.ts +0 -46
- package/dist/components/avatar/index.js +0 -19
- package/dist/components/badge/variants.d.ts +0 -33
- package/dist/components/badge/variants.js +0 -10
- package/dist/components/button/variants.d.ts +0 -39
- package/dist/components/button/variants.js +0 -8
- package/dist/components/inputs/color-picker/variants.d.ts +0 -61
- package/dist/components/inputs/color-picker/variants.js +0 -28
- package/dist/components/inputs/field/index.d.ts +0 -116
- package/dist/components/inputs/field/index.js +0 -29
- package/dist/components/inputs/file-dropzone/index.d.ts +0 -167
- package/dist/components/inputs/file-dropzone/index.js +0 -33
- package/dist/components/inputs/file-dropzone/variants.d.ts +0 -35
- package/dist/components/inputs/file-dropzone/variants.js +0 -12
- package/dist/components/inputs/label/index.d.ts +0 -32
- package/dist/components/inputs/label/index.js +0 -11
- package/dist/components/inputs/number-input/index.d.ts +0 -87
- package/dist/components/inputs/number-input/index.js +0 -21
- package/dist/components/inputs/select/variants.d.ts +0 -5
- package/dist/components/inputs/select/variants.js +0 -6
- package/dist/components/navigation/navigation-menu/index.d.ts +0 -126
- package/dist/components/navigation/navigation-menu/index.js +0 -41
- package/dist/components/navigation/pagination/index.d.ts +0 -75
- package/dist/components/navigation/pagination/index.js +0 -41
- package/dist/components/navigation/sidebar/variants.d.ts +0 -25
- package/dist/components/navigation/sidebar/variants.js +0 -28
- package/dist/components/progress/index.d.ts +0 -56
- package/dist/components/progress/index.js +0 -15
- package/dist/docs/index.d.ts +0 -83
- package/dist/docs/index.js +0 -288
- package/dist/docs/vite.d.ts +0 -31
- package/dist/docs/vite.js +0 -45
- package/dist/lib/date-locale.d.ts +0 -26
- package/dist/lib/date-locale.js +0 -24
- package/dist/lib/field-classes.d.ts +0 -149
- package/dist/lib/field-classes.js +0 -41
- package/dist/lib/field-variants.d.ts +0 -47
- package/dist/lib/field-variants.js +0 -26
- package/dist/lib/fluid-hover-highlight.d.ts +0 -112
- package/dist/lib/fluid-hover-highlight.js +0 -17
- package/dist/lib/focus-ring.d.ts +0 -18
- package/dist/lib/focus-ring.js +0 -11
- package/dist/lib/goo-indicator.d.ts +0 -46
- package/dist/lib/goo-indicator.js +0 -14
- package/dist/lib/icon-context.d.ts +0 -30
- package/dist/lib/icon-context.js +0 -12
- package/dist/lib/inert.d.ts +0 -9
- package/dist/lib/inert.js +0 -8
- package/dist/lib/morph-layers.d.ts +0 -93
- package/dist/lib/morph-layers.js +0 -13
- package/dist/lib/morph-part.d.ts +0 -21
- package/dist/lib/morph-part.js +0 -12
- package/dist/lib/popup.d.ts +0 -20
- package/dist/lib/popup.js +0 -12
- package/dist/lib/reduced-motion.d.ts +0 -9
- package/dist/lib/reduced-motion.js +0 -6
- package/dist/lib/row-index.d.ts +0 -10
- package/dist/lib/row-index.js +0 -8
- package/dist/lib/row-order.d.ts +0 -28
- package/dist/lib/row-order.js +0 -10
- package/dist/lib/scroll-column.d.ts +0 -31
- package/dist/lib/scroll-column.js +0 -19
- package/dist/lib/shape-context.d.ts +0 -30
- package/dist/lib/shape-context.js +0 -13
- package/dist/lib/shape-radius.d.ts +0 -19
- package/dist/lib/shape-radius.js +0 -6
- package/dist/lib/size-context.d.ts +0 -94
- package/dist/lib/size-context.js +0 -20
- package/dist/lib/size-scale.d.ts +0 -79
- package/dist/lib/size-scale.js +0 -8
- package/dist/lib/slot.d.ts +0 -39
- package/dist/lib/slot.js +0 -14
- package/dist/lib/springs.d.ts +0 -44
- package/dist/lib/springs.js +0 -8
- package/dist/lib/surface-classes.d.ts +0 -19
- package/dist/lib/surface-classes.js +0 -10
- package/dist/lib/surface-context.d.ts +0 -9
- package/dist/lib/surface-context.js +0 -8
- package/dist/lib/use-controllable-state.d.ts +0 -2
- package/dist/lib/use-controllable-state.js +0 -6
- package/dist/lib/use-fluid-hover.d.ts +0 -138
- package/dist/lib/use-fluid-hover.js +0 -14
- package/dist/lib/use-is-mobile.d.ts +0 -2
- package/dist/lib/use-is-mobile.js +0 -6
- package/dist/lib/use-keyboard-nav-gate.d.ts +0 -20
- package/dist/lib/use-keyboard-nav-gate.js +0 -7
- package/dist/lib/use-merge-split.d.ts +0 -47
- package/dist/lib/use-merge-split.js +0 -15
- package/dist/lib/use-morph.d.ts +0 -101
- package/dist/lib/use-morph.js +0 -20
- package/dist/lib/use-touch-primary.d.ts +0 -2
- package/dist/lib/use-touch-primary.js +0 -6
- package/dist/lib/utils.d.ts +0 -3
- package/dist/lib/utils.js +0 -6
- package/dist/primitives/separator/index.d.ts +0 -25
- package/dist/primitives/separator/index.js +0 -9
- package/dist/primitives/skeleton/index.d.ts +0 -35
- package/dist/primitives/skeleton/index.js +0 -13
- package/dist/shared/components-124epb7f.js +0 -53
- package/dist/shared/components-1kab3j44.js +0 -337
- package/dist/shared/components-26snnbs9.js +0 -322
- package/dist/shared/components-34txk9rj.js +0 -35
- package/dist/shared/components-3h92d94m.js +0 -23
- package/dist/shared/components-4vfm04wq.js +0 -82
- package/dist/shared/components-54nq988h.js +0 -123
- package/dist/shared/components-7s7pykk5.js +0 -281
- package/dist/shared/components-7zkhz3r8.js +0 -399
- package/dist/shared/components-8v644a0s.js +0 -298
- package/dist/shared/components-a4evvq8h.js +0 -271
- package/dist/shared/components-adkarj8n.js +0 -35
- package/dist/shared/components-b1e239g3.js +0 -28
- package/dist/shared/components-b94t6qtj.js +0 -143
- package/dist/shared/components-c9yr4ed1.js +0 -44
- package/dist/shared/components-cwg1d0w6.js +0 -254
- package/dist/shared/components-d3shbwqq.js +0 -95
- package/dist/shared/components-dhgk6mbt.js +0 -16
- package/dist/shared/components-drxrtnp2.js +0 -25
- package/dist/shared/components-ej4r7wmj.js +0 -482
- package/dist/shared/components-f7h57m6t.js +0 -59
- package/dist/shared/components-fejdzvqc.js +0 -448
- package/dist/shared/components-gx4h5rbb.js +0 -40
- package/dist/shared/components-gyt7my41.js +0 -328
- package/dist/shared/components-h6p86be3.js +0 -19
- package/dist/shared/components-h83az0bz.js +0 -33
- package/dist/shared/components-hdzrqqpx.js +0 -55
- package/dist/shared/components-krtx3m5b.js +0 -84
- package/dist/shared/components-kswxnm3h.js +0 -24
- package/dist/shared/components-mpa2y6x8.js +0 -277
- package/dist/shared/components-mrbaxt8q.js +0 -62
- package/dist/shared/components-pt5ftm4p.js +0 -38
- package/dist/shared/components-rpw4md2b.js +0 -179
- package/dist/shared/components-rtadhd9j.js +0 -64
- package/dist/shared/components-tz4gaf1v.js +0 -23
- package/dist/shared/components-wbbckbw8.js +0 -34
- package/dist/shared/components-xdqmt6ed.js +0 -20
- package/dist/shared/components-xjkdvnbg.js +0 -107
- package/dist/shared/components-z3v0n0de.js +0 -11
- package/dist/shared/components-z6dfqtyt.js +0 -1545
- package/dist/tokens/colors.css +0 -230
- package/dist/tokens/motion.css +0 -40
- package/dist/tokens/radius.css +0 -10
- package/dist/tokens/spacing.css +0 -9
- package/dist/tokens/typography.css +0 -119
- package/docs/chrome.css +0 -120
- package/docs/chrome.tsx +0 -283
- package/docs/index.ts +0 -9
- package/docs/vite.ts +0 -85
- package/index.ts +0 -60
- package/lib/date-locale.ts +0 -195
- package/lib/field-classes.ts +0 -121
- package/lib/field-variants.ts +0 -117
- package/lib/fluid-hover-highlight.tsx +0 -139
- package/lib/focus-ring.tsx +0 -46
- package/lib/goo-indicator.tsx +0 -261
- package/lib/icon-context.tsx +0 -310
- package/lib/inert.ts +0 -22
- package/lib/morph-layers.tsx +0 -102
- package/lib/morph-part.tsx +0 -49
- package/lib/popup.ts +0 -52
- package/lib/reduced-motion.ts +0 -19
- package/lib/row-index.tsx +0 -29
- package/lib/row-order.tsx +0 -126
- package/lib/scroll-column.tsx +0 -273
- package/lib/shape-context.tsx +0 -138
- package/lib/shape-radius.ts +0 -67
- package/lib/size-context.tsx +0 -66
- package/lib/size-scale.ts +0 -115
- package/lib/slot.ts +0 -115
- package/lib/springs.ts +0 -62
- package/lib/surface-classes.ts +0 -58
- package/lib/surface-context.tsx +0 -21
- package/lib/use-controllable-state.ts +0 -44
- package/lib/use-fluid-hover.ts +0 -541
- package/lib/use-is-mobile.ts +0 -25
- package/lib/use-keyboard-nav-gate.ts +0 -45
- package/lib/use-merge-split.tsx +0 -209
- package/lib/use-morph.ts +0 -557
- package/lib/use-touch-primary.tsx +0 -43
- package/lib/utils.ts +0 -37
- package/preset.ts +0 -199
- package/primitives/code/examples.mdx +0 -27
- package/primitives/code/index.tsx +0 -80
- package/primitives/craft/demos.tsx +0 -173
- package/primitives/craft/examples.mdx +0 -142
- package/primitives/craft/index.tsx +0 -76
- package/primitives/fluid-hover/examples.mdx +0 -84
- package/primitives/fluid-hover/index.tsx +0 -316
- package/primitives/morph/examples.mdx +0 -139
- package/primitives/morph/index.tsx +0 -192
- package/primitives/motion/examples.mdx +0 -100
- package/primitives/motion/index.tsx +0 -160
- package/primitives/scroll-area/examples.mdx +0 -58
- package/primitives/scroll-area/index.tsx +0 -204
- package/primitives/separator/examples.mdx +0 -30
- package/primitives/separator/index.tsx +0 -53
- package/primitives/sizes/examples.mdx +0 -167
- package/primitives/sizes/index.tsx +0 -89
- package/primitives/skeleton/examples.mdx +0 -51
- package/primitives/skeleton/index.tsx +0 -72
- package/primitives/skeleton/skeleton.css +0 -15
- package/primitives/surface/examples.mdx +0 -173
- package/primitives/surface/index.tsx +0 -109
- package/primitives/surface/surface.css +0 -176
- /package/dist/shared/{components-3ey0hzpb.js → components-1v7rxytx.js} +0 -0
- /package/dist/shared/{components-jcnjay5g.js → components-ezxhrmer.js} +0 -0
|
@@ -1,1527 +0,0 @@
|
|
|
1
|
-
// biome-ignore-all lint: vendored upstream code keeps its own patterns (see the header)
|
|
2
|
-
/*
|
|
3
|
-
* Ported from Fluid Functionalism — github.com/mickadesign/fluid-functionalism
|
|
4
|
-
* `registry/base/combobox.tsx` @ b3587bdbd83fc66c2a6aae3817ffb856cb09260b
|
|
5
|
-
* MIT License © 2026 Micka Touillaud — fluidfunctionalism.com (notice: LICENSE.fluid-functionalism)
|
|
6
|
-
* Local modifications:
|
|
7
|
-
* - `"use client"` dropped (no RSC here).
|
|
8
|
-
* - Imports rewritten: `@/lib/*` → `../../../lib/*` (`SizeProvider` →
|
|
9
|
-
* `../../../primitives/sizes`), `@/hooks/*` → `../../../lib/*`,
|
|
10
|
-
* `@/lib/elevated` → `../../../primitives/surface`, `@/components/ui/scroll-area` →
|
|
11
|
-
* `../../../primitives/scroll-area`, `@/components/ui/fluid-hover-highlight` →
|
|
12
|
-
* `../../../lib/fluid-hover-highlight`, `framer-motion` → `motion/react`.
|
|
13
|
-
* - `React.*` type references replaced with named type imports (no React namespace import).
|
|
14
|
-
* - The root's generics (`<T extends ComboboxItemData, Multiple extends boolean>`) dropped:
|
|
15
|
-
* modo's parser needs a plain (non-generic) function declaration for the root plus a
|
|
16
|
-
* same-file `interface ComboboxProps`. Item objects gained an index signature so rows can still
|
|
17
|
-
* carry extra fields (`icon`, …), `value`/`defaultValue` are `string | string[]`, and
|
|
18
|
-
* `onValueChange` is a union of the two handler shapes so `setState` still assigns.
|
|
19
|
-
* `ComboboxValue<Multiple>` is still exported for callers that spell the value out.
|
|
20
|
-
* - Uncontrolled create mode added: `creatable` offers the create row with no callback and
|
|
21
|
-
* the component keeps the items it makes (merged into `items`, deduped by value), so a
|
|
22
|
-
* combobox can grow its own list. `onCreate` is unchanged and still wins when it returns
|
|
23
|
-
* an item.
|
|
24
|
-
* - `noUncheckedIndexedAccess`: the single-mode checked rect guards `checkedIndices[0]`.
|
|
25
|
-
* - modo item: TSDoc (FF docs page + API tables), statics `Combobox.Input` / `.Chips` /
|
|
26
|
-
* `.Content` / `.List` / `.Item` / `.Empty` assigned on the root, upstream named exports
|
|
27
|
-
* kept, `export default Combobox`.
|
|
28
|
-
* - Styling reads DS tokens (AGENTS.md styling): `text-[Npx]` →
|
|
29
|
-
* `text-<role>[-compact]`; the hex focus-ring fallback →
|
|
30
|
-
* `ring-focus-ring` / `border-focus-ring`; `duration-80|120|160` and
|
|
31
|
-
* tier-length JS durations → `duration-<tier>` / `spring.*`.
|
|
32
|
-
* - The popup morphs out of the field through the shared morph layer
|
|
33
|
-
* (`lib/use-morph.ts` + `MorphSurface`, goo by default on
|
|
34
|
-
* `spring.goo`; `Combobox.Content` takes `from` / `effect` /
|
|
35
|
-
* `hideSource` / `tier`). Whatever opened it (typing, a press on the input
|
|
36
|
-
* or the chevron), the source is the field the list anchors to. The
|
|
37
|
-
* `scaleY` motion wrapper and the `actionsRef` deferred unmount (with its
|
|
38
|
-
* fallback timer) are gone: the morph holds Base UI's unmount itself. The
|
|
39
|
-
* popup paints its level through the morph's surface layers instead of
|
|
40
|
-
* `render={<Elevated/>}`, re-providing it with `SurfaceProvider`.
|
|
41
|
-
* - `effect="morph"` with `hideSource` opens the list in place over its
|
|
42
|
-
* trigger: the positioner's `sideOffset` becomes the engine's `inPlaceOffset`.
|
|
43
|
-
* - The fields' ladder is the shared `lib/field-classes.ts` recipe (the local
|
|
44
|
-
* `fieldVariants` cva moved there, still exported here as
|
|
45
|
-
* `comboboxFieldVariants`; its default is now `borderless`, the fields pass
|
|
46
|
-
* `bordered`): `error` stamps `data-invalid` instead of overriding the ring
|
|
47
|
-
* per state, so an invalid field also tints its lit fill; the leading icon
|
|
48
|
-
* darkens and thickens on hover too, not only on focus. Both fields build
|
|
49
|
-
* their box with the shared `useFieldBox` (ladder in the variant, state
|
|
50
|
-
* attributes, radius, inset), as every text field does; `variant` is typed
|
|
51
|
-
* `FieldVariant` (no `null`).
|
|
52
|
-
* - Hooks for remote data (local): `loading` (the chevron turns into a
|
|
53
|
-
* spinner, a status row under the list, the empty message held back),
|
|
54
|
-
* a controlled query (`inputValue` / `defaultInputValue` /
|
|
55
|
-
* `onInputValueChange`), `filter={null}` to keep every item the caller
|
|
56
|
-
* hands in, `readOnly`, a controlled popup (`open` / `defaultOpen` /
|
|
57
|
-
* `onOpenChange`), a `footer` under the list (`Combobox.Content`), and
|
|
58
|
-
* object items whose `label` is any node with a `textValue` to filter and
|
|
59
|
-
* fill the field by.
|
|
60
|
-
* - Field parity (local): the fields read a surrounding Field's invalid state
|
|
61
|
-
* (`error` still forces it; `invalid` is a prop too, and `aria-invalid`
|
|
62
|
-
* follows the resolved state), `hideTrigger` drops the chevron button,
|
|
63
|
-
* `labels` on the root translates the "Open" / "Clear" / "Remove …" button
|
|
64
|
-
* names and the default "Search…" placeholder, and `onInputValueChange`
|
|
65
|
-
* hears Base UI's change details (`reason`) as a second argument.
|
|
66
|
-
*/
|
|
67
|
-
|
|
68
|
-
import { Combobox as ComboboxPrimitive, type ComboboxRootChangeEventDetails } from '@base-ui/react/combobox'
|
|
69
|
-
import { AnimatePresence, animate, motion, useMotionValue } from 'motion/react'
|
|
70
|
-
import {
|
|
71
|
-
createContext,
|
|
72
|
-
forwardRef,
|
|
73
|
-
type HTMLAttributes,
|
|
74
|
-
type InputHTMLAttributes,
|
|
75
|
-
type MutableRefObject,
|
|
76
|
-
type ReactNode,
|
|
77
|
-
type RefObject,
|
|
78
|
-
useCallback,
|
|
79
|
-
useContext,
|
|
80
|
-
useEffect,
|
|
81
|
-
useImperativeHandle,
|
|
82
|
-
useMemo,
|
|
83
|
-
useRef,
|
|
84
|
-
useState,
|
|
85
|
-
} from 'react'
|
|
86
|
-
import { type FieldVariant, fieldIconClasses, fieldVariants, useFieldBox } from '../../../lib/field-classes'
|
|
87
|
-
import { FluidHoverHighlight } from '../../../lib/fluid-hover-highlight'
|
|
88
|
-
import { type IconComponent, useIcon } from '../../../lib/icon-context'
|
|
89
|
-
import { MorphSurface } from '../../../lib/morph-layers'
|
|
90
|
-
import { isDisabledRow, popupScrollAreaClass, popupViewportClass } from '../../../lib/popup'
|
|
91
|
-
import { useReduceMotion } from '../../../lib/reduced-motion'
|
|
92
|
-
import { shapeMap, useShape } from '../../../lib/shape-context'
|
|
93
|
-
import { type SizeVariant, useSize } from '../../../lib/size-context'
|
|
94
|
-
import { spring } from '../../../lib/springs'
|
|
95
|
-
import { SURFACE_BG, SURFACE_SHADOW } from '../../../lib/surface-classes'
|
|
96
|
-
import { SurfaceProvider, useSurface } from '../../../lib/surface-context'
|
|
97
|
-
import { useControllableState } from '../../../lib/use-controllable-state'
|
|
98
|
-
import { useFluidHover, useRegisterFluidHoverItem } from '../../../lib/use-fluid-hover'
|
|
99
|
-
import { SelectionBackgrounds, useMergeSplitBlocks, useSelectionRuns } from '../../../lib/use-merge-split'
|
|
100
|
-
import { inPlaceOffset, type MorphOrigin, opensInPlace, useMorph, useMorphOrigin } from '../../../lib/use-morph'
|
|
101
|
-
import { cn } from '../../../lib/utils'
|
|
102
|
-
import { ScrollArea } from '../../../primitives/scroll-area'
|
|
103
|
-
import { SizeProvider } from '../../../primitives/sizes'
|
|
104
|
-
|
|
105
|
-
// ---------------------------------------------------------------------------
|
|
106
|
-
// Combobox
|
|
107
|
-
//
|
|
108
|
-
// A text field that filters a list as you type. Built on Base UI's Combobox
|
|
109
|
-
// primitive, which owns the filtering, the combobox/listbox ARIA wiring
|
|
110
|
-
// (aria-activedescendant — the input keeps focus while arrows move a
|
|
111
|
-
// highlight through the rows), positioning, dismissal, and the hidden form
|
|
112
|
-
// input. This layer keeps the fluid-hover overlays, the morph open/close
|
|
113
|
-
// (lib/use-morph.ts), and the animated checkmark — the same visuals as
|
|
114
|
-
// Select.
|
|
115
|
-
//
|
|
116
|
-
// Items are data: pass `items` to the root and render rows from the
|
|
117
|
-
// ComboboxList function child. String items are their own value and label;
|
|
118
|
-
// object items carry `{ value, label }` plus anything else you need.
|
|
119
|
-
// ---------------------------------------------------------------------------
|
|
120
|
-
|
|
121
|
-
/** An object item: `value` and `label`, plus whatever the rows need. */
|
|
122
|
-
interface ComboboxItemObject {
|
|
123
|
-
value: string
|
|
124
|
-
/** What the row and the chip show: text, or any node with a `textValue`. */
|
|
125
|
-
label: ReactNode
|
|
126
|
-
/** The item as text: what the query filters and the field shows. Defaults to `label` when it is a string, else `value`. */
|
|
127
|
-
textValue?: string
|
|
128
|
-
[key: string]: unknown
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
type ComboboxItemData = string | ComboboxItemObject
|
|
132
|
-
|
|
133
|
-
function itemValue(item: ComboboxItemData): string {
|
|
134
|
-
return typeof item === 'string' ? item : item.value
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** The item as text: what filters, fills the field and names a chip. */
|
|
138
|
-
function itemLabel(item: ComboboxItemData): string {
|
|
139
|
-
if (typeof item === 'string') return item
|
|
140
|
-
return item.textValue ?? (typeof item.label === 'string' ? item.label : item.value)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** The item as shown: its label node. */
|
|
144
|
-
function itemNode(item: ComboboxItemData): ReactNode {
|
|
145
|
-
return typeof item === 'string' ? item : item.label
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/** Every fixed word the field's buttons announce, for translating it. */
|
|
149
|
-
interface ComboboxLabels {
|
|
150
|
-
/** Names the chevron button. Defaults to `"Open"`. */
|
|
151
|
-
open?: string
|
|
152
|
-
/** Names the clear ✕ of a `clearable` field. Defaults to `"Clear"`. */
|
|
153
|
-
clear?: string
|
|
154
|
-
/** Names a chip's remove ✕ from the chip's text: `(label) => …`. Defaults to ``(label) => `Remove ${label}` ``. */
|
|
155
|
-
remove?: (label: string) => string
|
|
156
|
-
/** The fields' placeholder while nothing is typed or selected, unless a field sets its own. Defaults to `"Search…"`. */
|
|
157
|
-
placeholder?: string
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** What `Combobox.Content`'s footer gets when it is a function. */
|
|
161
|
-
interface ComboboxFooterContext {
|
|
162
|
-
/** Closes the popup. */
|
|
163
|
-
close: () => void
|
|
164
|
-
/** The trimmed query. */
|
|
165
|
-
query: string
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** A node, or a function of the popup's state, rendered under the list. */
|
|
169
|
-
type ComboboxFooter = ReactNode | ((context: ComboboxFooterContext) => ReactNode)
|
|
170
|
-
|
|
171
|
-
// The create row is one more item in the list the primitive filters and
|
|
172
|
-
// highlights, so Enter and the arrows reach it like any row. Its value can't
|
|
173
|
-
// collide with a consumer's; its label is the query it would create.
|
|
174
|
-
const CREATE_VALUE = '\0create'
|
|
175
|
-
|
|
176
|
-
function isCreateItem(item: ComboboxItemData): boolean {
|
|
177
|
-
return itemValue(item) === CREATE_VALUE
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function defaultCreateLabel(query: string): ReactNode {
|
|
181
|
-
return `Create “${query}”`
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
type ComboboxValue<Multiple extends boolean> = Multiple extends true ? string[] : string
|
|
185
|
-
|
|
186
|
-
interface ComboboxContextValue {
|
|
187
|
-
/** Selected values — one entry in single mode, any number in multiple. */
|
|
188
|
-
values: string[]
|
|
189
|
-
multiple: boolean
|
|
190
|
-
inputValue: string
|
|
191
|
-
open: boolean
|
|
192
|
-
/** What opened the popup; its trigger is always the field (`anchorRef`). */
|
|
193
|
-
origin: RefObject<MorphOrigin>
|
|
194
|
-
/** The field (input group or chips container) the popup anchors to.
|
|
195
|
-
* `RefObject<HTMLDivElement>` (not `… | null`) so the ref prop on Base UI's
|
|
196
|
-
* InputGroup/Chips accepts it under @types/react 18. */
|
|
197
|
-
anchorRef: RefObject<HTMLDivElement>
|
|
198
|
-
disabled: boolean
|
|
199
|
-
itemsByValue: Map<string, ComboboxItemData>
|
|
200
|
-
/** The create row's label for the current query; null while no row is
|
|
201
|
-
* offered. */
|
|
202
|
-
createRow: ReactNode | null
|
|
203
|
-
/** `hideSelected` has emptied the list with nothing typed: every item is
|
|
204
|
-
* a chip already. */
|
|
205
|
-
allSelected: boolean
|
|
206
|
-
/** Local: items are on their way; the empty message waits. */
|
|
207
|
-
loading: boolean
|
|
208
|
-
/** Local: the loading row's text. */
|
|
209
|
-
loadingLabel: ReactNode
|
|
210
|
-
/** Local: shows the picks but takes no input. */
|
|
211
|
-
readOnly: boolean
|
|
212
|
-
/** Local: opens or closes the popup (the footer's `close`). */
|
|
213
|
-
setOpen: (open: boolean) => void
|
|
214
|
-
/** Local: the translated button names and default placeholder. */
|
|
215
|
-
labels: ComboboxLabels
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const NO_LABELS: ComboboxLabels = {}
|
|
219
|
-
|
|
220
|
-
const ComboboxContext = createContext<ComboboxContextValue | null>(null)
|
|
221
|
-
|
|
222
|
-
/** Highlighted row (the primitive's active index over the rendered rows)
|
|
223
|
-
* and whether a keyboard/auto highlight put it there — pointer highlights
|
|
224
|
-
* are left to fluid hover. Its own context so a per-pointer highlight
|
|
225
|
-
* re-renders the list, not the field and every row. */
|
|
226
|
-
interface Highlight {
|
|
227
|
-
index: number
|
|
228
|
-
keyboard: boolean
|
|
229
|
-
}
|
|
230
|
-
const ComboboxHighlightContext = createContext<Highlight | null>(null)
|
|
231
|
-
|
|
232
|
-
function useComboboxContext() {
|
|
233
|
-
const ctx = useContext(ComboboxContext)
|
|
234
|
-
if (!ctx) throw new Error('Combobox compound components must be inside <Combobox>')
|
|
235
|
-
return ctx
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Content context for fluid hover
|
|
239
|
-
interface ComboboxContentContextValue {
|
|
240
|
-
registerItem: (index: number, element: HTMLElement | null) => void
|
|
241
|
-
activeIndex: number | null
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const ComboboxContentContext = createContext<ComboboxContentContextValue | null>(null)
|
|
245
|
-
|
|
246
|
-
// Each rendered row learns its index from the list, so consumers never
|
|
247
|
-
// number rows by hand.
|
|
248
|
-
const ComboboxItemIndexContext = createContext<number>(0)
|
|
249
|
-
|
|
250
|
-
// ---------------------------------------------------------------------------
|
|
251
|
-
// Combobox (root)
|
|
252
|
-
// ---------------------------------------------------------------------------
|
|
253
|
-
|
|
254
|
-
interface ComboboxProps {
|
|
255
|
-
/** `Combobox.Input` or `Combobox.Chips`, plus `Combobox.Content`. */
|
|
256
|
-
children: ReactNode
|
|
257
|
-
/** The options. A string is its own value and label; an object carries `value`, `label`, and whatever your rows need. */
|
|
258
|
-
items: readonly ComboboxItemData[]
|
|
259
|
-
/** Any number of picks. Values become `string[]`; use `Combobox.Chips` for the field. Defaults to `false`. */
|
|
260
|
-
multiple?: boolean
|
|
261
|
-
/** Selected value. `""` means none; an array when `multiple`. Controlled — pair it with `onValueChange`. */
|
|
262
|
-
value?: string | string[]
|
|
263
|
-
/** Selection on first render when uncontrolled. Defaults to none. */
|
|
264
|
-
defaultValue?: string | string[]
|
|
265
|
-
/** Called with the pick, `""` when cleared, or the array when `multiple`. */
|
|
266
|
-
onValueChange?: ((value: string) => void) | ((value: string[]) => void)
|
|
267
|
-
/** Match an item against the query, or `null` to show every item as given (filtered elsewhere, e.g. on a server). Defaults to a case-insensitive "contains" on the item's text. */
|
|
268
|
-
filter?: ((item: ComboboxItemData, query: string) => boolean) | null
|
|
269
|
-
/** Controlled query, the field's text. Pair it with `onInputValueChange`. */
|
|
270
|
-
inputValue?: string
|
|
271
|
-
/** Initial query, for an uncontrolled field. Defaults to the picked item's text in single mode, else `""`. */
|
|
272
|
-
defaultInputValue?: string
|
|
273
|
-
/** Called with the field's text on every keystroke, and when a pick or a close rewrites it. The second argument is Base UI's change details: `reason` tells typing (`"input-change"`) from a pick, a clear or a close rewriting the text (`"item-press"`, `"clear-press"`, `"none"`, …). */
|
|
274
|
-
onInputValueChange?: (inputValue: string, details: ComboboxRootChangeEventDetails) => void
|
|
275
|
-
/** Items are loading: the chevron turns into a spinner, a status row sits under the list and the empty message waits. Defaults to `false`. */
|
|
276
|
-
loading?: boolean
|
|
277
|
-
/** The loading row's text. Defaults to `"Loading…"`. */
|
|
278
|
-
loadingLabel?: ReactNode
|
|
279
|
-
/** Shows the picks but takes no input: the popup stays shut and the clear ✕ and chip ✕ go. Defaults to `false`. */
|
|
280
|
-
readOnly?: boolean
|
|
281
|
-
/** Controlled open state of the popup. Pair it with `onOpenChange`. */
|
|
282
|
-
open?: boolean
|
|
283
|
-
/** Initial open state, for an uncontrolled popup. Defaults to `false`. */
|
|
284
|
-
defaultOpen?: boolean
|
|
285
|
-
/** Called when the popup opens or closes. */
|
|
286
|
-
onOpenChange?: (open: boolean) => void
|
|
287
|
-
/** Adds a last row that creates the trimmed query whenever no label matches it exactly. Add the item to `items` and return it to select it. */
|
|
288
|
-
onCreate?: (query: string) => ComboboxItemData | void
|
|
289
|
-
/** The create row's label. Defaults to ``(query) => `Create “${query}”` ``. */
|
|
290
|
-
createLabel?: (query: string) => ReactNode
|
|
291
|
-
/** Local addition: offer the create row with no callback — the component keeps the items it makes, so nothing outside has to hold the list. Defaults to `false`. */
|
|
292
|
-
creatable?: boolean
|
|
293
|
-
/** Multiple only. Picked items leave the list, so it reads as what is left to add. Deselect from the chips. Defaults to `false`. */
|
|
294
|
-
hideSelected?: boolean
|
|
295
|
-
/** Disables the field and the popup. Defaults to `false`. */
|
|
296
|
-
disabled?: boolean
|
|
297
|
-
/** Form field name; a hidden input carries the value. */
|
|
298
|
-
name?: string
|
|
299
|
-
/** Marks the hidden form input required. */
|
|
300
|
-
required?: boolean
|
|
301
|
-
/** Pins field and popup to one step of the size ladder (default 36px, compact 28px — see the Sizes primitive). Omitted, both follow the surrounding SizeProvider. */
|
|
302
|
-
size?: SizeVariant
|
|
303
|
-
/** Every fixed word the field's buttons announce (`open`, `clear`, `remove`, `placeholder`), for translating it. Defaults to English: `"Open"`, `"Clear"`, `"Remove …"`, `"Search…"`. */
|
|
304
|
-
labels?: ComboboxLabels
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function toValues(v: string | readonly string[] | undefined): string[] {
|
|
308
|
-
if (v === undefined) return []
|
|
309
|
-
if (Array.isArray(v)) return v as string[]
|
|
310
|
-
return v === '' ? [] : [v as string]
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Type to filter a list. 1 pick, or chips.
|
|
315
|
-
*
|
|
316
|
-
* Items are data on the root: strings, which are their own value and label,
|
|
317
|
-
* or `{ value, label, … }` objects. `Combobox.List` takes a function child and
|
|
318
|
-
* renders one `Combobox.Item` per match, so the filtering, the ARIA wiring
|
|
319
|
-
* (the input keeps focus while the arrows move a highlight through the rows),
|
|
320
|
-
* the positioning and the hidden form input stay with Base UI. On top of that
|
|
321
|
-
* sit the fluid-hover highlight, the merged selection background of
|
|
322
|
-
* `Checkbox.Group`, and a list that oozes out of the field and back through the
|
|
323
|
-
* shared morph (see Morph), goo by default. `multiple` turns the picks into a
|
|
324
|
-
* `string[]` and pairs with `Combobox.Chips`: one chip per pick, Backspace in
|
|
325
|
-
* an empty field drops the last one. Uncontrolled with `defaultValue`, or
|
|
326
|
-
* controlled with `value` plus `onValueChange`.
|
|
327
|
-
*
|
|
328
|
-
* For items that come from elsewhere (a server, a paged API), own the query
|
|
329
|
-
* with `inputValue` + `onInputValueChange`, pass `filter={null}` so the list
|
|
330
|
-
* shows exactly the items you hand it, and set `loading` while they are on
|
|
331
|
-
* their way: the chevron blurs into a spinner and a status row sits under
|
|
332
|
-
* the list instead of the empty message. `Combobox.Content`'s `footer` pins
|
|
333
|
-
* an action under the list ("Load more", "Add new…"). An object item's
|
|
334
|
-
* `label` may be any node; its `textValue` is what filters, fills the field
|
|
335
|
-
* and names its chip. `readOnly` shows the picks and takes no input. Inside a
|
|
336
|
-
* Field the fields take its invalid state, like Input does, and `hideTrigger`
|
|
337
|
-
* drops the chevron (and with it the loading spinner; the popup's status row is then the only loading signal) for a field that opens by typing. The words the buttons
|
|
338
|
-
* announce ("Open", "Clear", "Remove …") and the default "Search…" placeholder
|
|
339
|
-
* come from `labels`, for translating them.
|
|
340
|
-
*
|
|
341
|
-
* Statics:
|
|
342
|
-
* - `Combobox.Input` — the single-pick field: icon, text input, clear ✕, chevron (`hideTrigger` drops it); `invalid` or `error` turn it destructive, as a surrounding Field's invalid state does.
|
|
343
|
-
* - `Combobox.Chips` — the multiple field: one chip per pick ahead of the input, with the same `hideTrigger`, `invalid` and `error`.
|
|
344
|
-
* - `Combobox.Content` — the popup surface, holding `Combobox.Empty` and `Combobox.List`:
|
|
345
|
-
* `side`, `align`, `sideOffset`, a `footer` under the list, and the morph options
|
|
346
|
-
* `from`, `effect`, `hideSource`, `tier`.
|
|
347
|
-
* - `Combobox.List` — a row per match, from a `(item, index) => ReactNode` child.
|
|
348
|
-
* - `Combobox.Item` — one row: `value`, optional `icon` and `disabled`.
|
|
349
|
-
* - `Combobox.Empty` — what shows when nothing matches.
|
|
350
|
-
*
|
|
351
|
-
* @example {@include ./examples.mdx}
|
|
352
|
-
*/
|
|
353
|
-
function Combobox({
|
|
354
|
-
children,
|
|
355
|
-
items,
|
|
356
|
-
multiple,
|
|
357
|
-
value,
|
|
358
|
-
defaultValue,
|
|
359
|
-
onValueChange,
|
|
360
|
-
filter,
|
|
361
|
-
onCreate,
|
|
362
|
-
createLabel = defaultCreateLabel,
|
|
363
|
-
creatable = false,
|
|
364
|
-
hideSelected = false,
|
|
365
|
-
disabled = false,
|
|
366
|
-
name,
|
|
367
|
-
required,
|
|
368
|
-
inputValue: inputValueProp,
|
|
369
|
-
defaultInputValue,
|
|
370
|
-
onInputValueChange,
|
|
371
|
-
labels,
|
|
372
|
-
loading = false,
|
|
373
|
-
loadingLabel = 'Loading…',
|
|
374
|
-
readOnly = false,
|
|
375
|
-
open: openProp,
|
|
376
|
-
defaultOpen = false,
|
|
377
|
-
onOpenChange,
|
|
378
|
-
size,
|
|
379
|
-
}: ComboboxProps) {
|
|
380
|
-
const isMultiple = !!multiple
|
|
381
|
-
const [internalValues, setInternalValues] = useState<string[]>(() => toValues(defaultValue))
|
|
382
|
-
// The primitive's change details ride along to the caller's handler: the setter below drops no-op
|
|
383
|
-
// writes, so the details are parked in a ref the handler reads when the write goes through.
|
|
384
|
-
const inputDetails = useRef<ComboboxRootChangeEventDetails | undefined>(undefined)
|
|
385
|
-
const [inputValue, setInputValue] = useControllableState(inputValueProp, defaultInputValue ?? '', next =>
|
|
386
|
-
onInputValueChange?.(next, inputDetails.current as ComboboxRootChangeEventDetails),
|
|
387
|
-
)
|
|
388
|
-
const [open, setOpen] = useControllableState(openProp, defaultOpen, onOpenChange)
|
|
389
|
-
const [highlight, setHighlight] = useState<Highlight | null>(null)
|
|
390
|
-
// Items the create row made while no consumer owns the list.
|
|
391
|
-
const [createdItems, setCreatedItems] = useState<ComboboxItemData[]>([])
|
|
392
|
-
const { origin, capture } = useMorphOrigin()
|
|
393
|
-
const anchorRef = useRef<HTMLDivElement | null>(null)
|
|
394
|
-
// Memoized on the prop identity: a string value is stable by nature and a
|
|
395
|
-
// consumer's array is state, so the derived array (and everything keyed on
|
|
396
|
-
// it) only changes when the selection does.
|
|
397
|
-
const controlledValues = useMemo(() => toValues(value), [value])
|
|
398
|
-
const values = value !== undefined ? controlledValues : internalValues
|
|
399
|
-
|
|
400
|
-
// The consumer's items plus anything `creatable` made here. A consumer that
|
|
401
|
-
// adds a created item to `items` itself wins — the internal copy is deduped
|
|
402
|
-
// away, so the two modes never double a row.
|
|
403
|
-
const allItems = useMemo<readonly ComboboxItemData[]>(() => {
|
|
404
|
-
const own = createdItems.filter(made => !items.some(item => itemValue(item) === itemValue(made)))
|
|
405
|
-
return own.length ? [...items, ...own] : items
|
|
406
|
-
}, [items, createdItems])
|
|
407
|
-
|
|
408
|
-
// The query the primitive filters on: the trimmed input. In single mode
|
|
409
|
-
// the field shows the selected label on open, which is an exact match, so
|
|
410
|
-
// no create row appears until the user types past it.
|
|
411
|
-
const query = inputValue.trim()
|
|
412
|
-
const canCreate = !!onCreate || creatable
|
|
413
|
-
const createItem = useMemo<ComboboxItemData | null>(() => {
|
|
414
|
-
if (!canCreate || query === '') return null
|
|
415
|
-
const lower = query.toLocaleLowerCase()
|
|
416
|
-
const exists = allItems.some(item => itemLabel(item).toLocaleLowerCase() === lower)
|
|
417
|
-
return exists ? null : { value: CREATE_VALUE, label: query }
|
|
418
|
-
}, [canCreate, query, allItems])
|
|
419
|
-
|
|
420
|
-
const itemsByValue = useMemo(() => {
|
|
421
|
-
const map = new Map<string, ComboboxItemData>()
|
|
422
|
-
for (const item of allItems) map.set(itemValue(item), item)
|
|
423
|
-
// Rows hand the primitive the item they were rendered from; the create
|
|
424
|
-
// row's carries the query it stands for.
|
|
425
|
-
if (createItem) map.set(CREATE_VALUE, createItem)
|
|
426
|
-
return map
|
|
427
|
-
}, [allItems, createItem])
|
|
428
|
-
const selectedItems = useMemo(
|
|
429
|
-
() => values.map(v => itemsByValue.get(v)).filter(Boolean) as ComboboxItemData[],
|
|
430
|
-
[values, itemsByValue],
|
|
431
|
-
)
|
|
432
|
-
|
|
433
|
-
// What the list shows: the items less the chips when `hideSelected`, plus
|
|
434
|
-
// the create row last, so Enter picks a real match while one exists and
|
|
435
|
-
// only creates once nothing matches.
|
|
436
|
-
const hideChecked = hideSelected && isMultiple
|
|
437
|
-
const listItems = useMemo<readonly ComboboxItemData[]>(() => {
|
|
438
|
-
const visible = hideChecked ? allItems.filter(item => !values.includes(itemValue(item))) : allItems
|
|
439
|
-
return createItem ? [...visible, createItem] : visible
|
|
440
|
-
}, [allItems, hideChecked, values, createItem])
|
|
441
|
-
const allSelected = hideChecked && query === '' && allItems.length > 0 && listItems.length === 0
|
|
442
|
-
|
|
443
|
-
// Base UI hands back the item(s); the public API speaks in values. A pick
|
|
444
|
-
// on the create row is not a selection: it asks the consumer for the item,
|
|
445
|
-
// and selects whatever comes back.
|
|
446
|
-
const onCreateRef = useRef(onCreate)
|
|
447
|
-
onCreateRef.current = onCreate
|
|
448
|
-
const handleValueChange = useCallback(
|
|
449
|
-
(next: ComboboxItemData[] | ComboboxItemData | null) => {
|
|
450
|
-
const picked = Array.isArray(next) ? next : next == null ? [] : [next]
|
|
451
|
-
const created = picked.find(isCreateItem)
|
|
452
|
-
let nextValues = picked.filter(item => !isCreateItem(item)).map(itemValue)
|
|
453
|
-
if (created) {
|
|
454
|
-
const madeQuery = itemLabel(created)
|
|
455
|
-
const made = onCreateRef.current?.(madeQuery) ?? undefined
|
|
456
|
-
if (made !== undefined) {
|
|
457
|
-
nextValues = [...nextValues, itemValue(made)]
|
|
458
|
-
} else if (creatable) {
|
|
459
|
-
// No consumer owns the list, so the item is made and kept here: the
|
|
460
|
-
// row it adds is indistinguishable from a real one.
|
|
461
|
-
const own: ComboboxItemData = { value: madeQuery, label: madeQuery }
|
|
462
|
-
setCreatedItems(prev => (prev.some(item => itemValue(item) === madeQuery) ? prev : [...prev, own]))
|
|
463
|
-
nextValues = [...nextValues, madeQuery]
|
|
464
|
-
} else if (!isMultiple) {
|
|
465
|
-
// Nothing to select: a single field keeps its pick.
|
|
466
|
-
return
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
if (value === undefined) setInternalValues(nextValues)
|
|
470
|
-
;(onValueChange as ((value: string | string[]) => void) | undefined)?.(
|
|
471
|
-
isMultiple ? nextValues : (nextValues[0] ?? ''),
|
|
472
|
-
)
|
|
473
|
-
},
|
|
474
|
-
[value, onValueChange, isMultiple, creatable],
|
|
475
|
-
)
|
|
476
|
-
|
|
477
|
-
// The create row must survive the filter that hides every other miss.
|
|
478
|
-
// Without a consumer filter the primitive's own collator match is kept.
|
|
479
|
-
const { contains } = ComboboxPrimitive.useFilter()
|
|
480
|
-
const filterFn = useMemo(() => {
|
|
481
|
-
// `null`: the caller filters; the list shows what it is handed.
|
|
482
|
-
if (filter === null) return null
|
|
483
|
-
if (!filter && !createItem) return undefined
|
|
484
|
-
const match = filter
|
|
485
|
-
? (item: ComboboxItemData, q: string) => filter(item, q)
|
|
486
|
-
: (item: ComboboxItemData, q: string) => contains(item, q, itemLabel)
|
|
487
|
-
return (item: ComboboxItemData, q: string) => isCreateItem(item) || match(item, q)
|
|
488
|
-
}, [filter, createItem, contains])
|
|
489
|
-
|
|
490
|
-
const createRow = createItem ? createLabel(query) : null
|
|
491
|
-
const ctx = useMemo(
|
|
492
|
-
() => ({
|
|
493
|
-
values,
|
|
494
|
-
multiple: isMultiple,
|
|
495
|
-
inputValue,
|
|
496
|
-
open,
|
|
497
|
-
origin,
|
|
498
|
-
anchorRef,
|
|
499
|
-
disabled,
|
|
500
|
-
itemsByValue,
|
|
501
|
-
createRow,
|
|
502
|
-
allSelected,
|
|
503
|
-
loading,
|
|
504
|
-
loadingLabel,
|
|
505
|
-
readOnly,
|
|
506
|
-
setOpen,
|
|
507
|
-
labels: labels ?? NO_LABELS,
|
|
508
|
-
}),
|
|
509
|
-
[
|
|
510
|
-
values,
|
|
511
|
-
isMultiple,
|
|
512
|
-
inputValue,
|
|
513
|
-
open,
|
|
514
|
-
origin,
|
|
515
|
-
disabled,
|
|
516
|
-
itemsByValue,
|
|
517
|
-
createRow,
|
|
518
|
-
allSelected,
|
|
519
|
-
loading,
|
|
520
|
-
loadingLabel,
|
|
521
|
-
readOnly,
|
|
522
|
-
setOpen,
|
|
523
|
-
labels,
|
|
524
|
-
],
|
|
525
|
-
)
|
|
526
|
-
|
|
527
|
-
// A size prop pins the whole compound (field + portalled popup — React
|
|
528
|
-
// context crosses portals) to one step of the ladder.
|
|
529
|
-
const root = (
|
|
530
|
-
<ComboboxContext.Provider value={ctx}>
|
|
531
|
-
<ComboboxHighlightContext.Provider value={highlight}>
|
|
532
|
-
<ComboboxPrimitive.Root
|
|
533
|
-
items={listItems}
|
|
534
|
-
multiple={isMultiple}
|
|
535
|
-
// Always controlled; "" (no selection) maps to Base UI's null. The
|
|
536
|
-
// value shape follows `multiple`, which the primitive's generics
|
|
537
|
-
// can't express from a boolean prop — hence the cast.
|
|
538
|
-
value={(isMultiple ? selectedItems : (selectedItems[0] ?? null)) as never}
|
|
539
|
-
onValueChange={handleValueChange as never}
|
|
540
|
-
isItemEqualToValue={(a: ComboboxItemData, b: ComboboxItemData) => itemValue(a) === itemValue(b)}
|
|
541
|
-
itemToStringLabel={itemLabel}
|
|
542
|
-
itemToStringValue={itemValue}
|
|
543
|
-
filter={filterFn}
|
|
544
|
-
open={open}
|
|
545
|
-
onOpenChange={(next, details) => {
|
|
546
|
-
// The list grows out of the field, whichever part opened it.
|
|
547
|
-
if (next) capture({ event: details.event, trigger: anchorRef.current ?? undefined })
|
|
548
|
-
setOpen(next)
|
|
549
|
-
}}
|
|
550
|
-
// Controlled only when the caller owns the query; otherwise Base UI
|
|
551
|
-
// keeps it and this mirrors it.
|
|
552
|
-
inputValue={inputValueProp}
|
|
553
|
-
defaultInputValue={defaultInputValue}
|
|
554
|
-
onInputValueChange={(next, details) => {
|
|
555
|
-
inputDetails.current = details
|
|
556
|
-
setInputValue(next)
|
|
557
|
-
}}
|
|
558
|
-
autoHighlight={ALWAYS_HIGHLIGHT}
|
|
559
|
-
onItemHighlighted={(item, details) =>
|
|
560
|
-
setHighlight(item === undefined ? null : { index: details.index, keyboard: details.reason !== 'pointer' })
|
|
561
|
-
}
|
|
562
|
-
disabled={disabled}
|
|
563
|
-
readOnly={readOnly}
|
|
564
|
-
name={name}
|
|
565
|
-
required={required}
|
|
566
|
-
// Non-modal: the page keeps scrolling and the Positioner tracks the
|
|
567
|
-
// anchor, so the popup follows its field instead of detaching.
|
|
568
|
-
modal={false}
|
|
569
|
-
>
|
|
570
|
-
{children}
|
|
571
|
-
</ComboboxPrimitive.Root>
|
|
572
|
-
</ComboboxHighlightContext.Provider>
|
|
573
|
-
</ComboboxContext.Provider>
|
|
574
|
-
)
|
|
575
|
-
|
|
576
|
-
return size ? <SizeProvider size={size}>{root}</SizeProvider> : root
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
Combobox.displayName = 'Combobox'
|
|
580
|
-
|
|
581
|
-
// ---------------------------------------------------------------------------
|
|
582
|
-
// ComboboxInput — the field: leading icon, text input, clear ✕, chevron.
|
|
583
|
-
// ---------------------------------------------------------------------------
|
|
584
|
-
|
|
585
|
-
// The field follows the global pill/rounded shape; the popup does not. Like
|
|
586
|
-
// Dropdown, the list keeps the smaller "rounded" radii whatever the rest of
|
|
587
|
-
// the UI is shaped: pill corners on a popover distort its padding and break
|
|
588
|
-
// the concentric fit of the rows' hover and selection backgrounds inside it.
|
|
589
|
-
const popupShape = shapeMap.rounded
|
|
590
|
-
|
|
591
|
-
// The first row is highlighted the moment the list opens and follows the
|
|
592
|
-
// query as it filters, so Enter always has a target. AriaCombobox accepts
|
|
593
|
-
// "always" for this; ComboboxRoot's prop type still says boolean.
|
|
594
|
-
const ALWAYS_HIGHLIGHT = 'always' as unknown as boolean
|
|
595
|
-
|
|
596
|
-
// The clear and chevron buttons share one quiet style; they sit inside the
|
|
597
|
-
// field's ring so they need no frame of their own.
|
|
598
|
-
const fieldButtonClass =
|
|
599
|
-
'flex shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none transition-colors duration-fast hover:text-foreground focus-visible:ring-1 focus-visible:ring-focus-ring data-[disabled]:pointer-events-none'
|
|
600
|
-
// The clear ✕ is a real icon button: the hover fill says "press me", where
|
|
601
|
-
// the chevron beside it only decorates the field it belongs to.
|
|
602
|
-
const clearButtonClass = cn(fieldButtonClass, 'hover:bg-hover active:bg-active transition-[color,background-color]')
|
|
603
|
-
// A chip's ✕ sits on the chip's own hover-tinted fill, so it steps up a notch.
|
|
604
|
-
const chipRemoveClass = cn(fieldButtonClass, 'rounded hover:bg-active')
|
|
605
|
-
|
|
606
|
-
interface ComboboxFieldProps
|
|
607
|
-
extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'value' | 'defaultValue' | 'onChange'> {
|
|
608
|
-
/** Framed at rest, or invisible until hovered or focused. Defaults to `"bordered"`. */
|
|
609
|
-
variant?: FieldVariant
|
|
610
|
-
/** Leading icon inside the field. */
|
|
611
|
-
icon?: IconComponent
|
|
612
|
-
/** Shown while nothing is typed or selected. Defaults to `labels.placeholder` on the root, else `"Search…"`. */
|
|
613
|
-
placeholder?: string
|
|
614
|
-
/** Error message under the field; tints the ring and sets `aria-invalid`. */
|
|
615
|
-
error?: string
|
|
616
|
-
/** Marks the value invalid without a message: the field turns destructive and sets `aria-invalid`. Defaults to the surrounding Field's invalid state. */
|
|
617
|
-
invalid?: boolean
|
|
618
|
-
/** Hides the chevron button, for a field that opens by typing or arrow keys alone. It also drops the loading spinner, which lives in that button, so the popup's status row is the only loading signal. Defaults to `false`. */
|
|
619
|
-
hideTrigger?: boolean
|
|
620
|
-
/** Render a ✕ that clears the selection and query. Its slot is always
|
|
621
|
-
* reserved so the field never changes width. Defaults to `false`. */
|
|
622
|
-
clearable?: boolean
|
|
623
|
-
/** Size override for the field alone. Prefer the `size` prop on <Combobox>
|
|
624
|
-
* (or a surrounding SizeProvider) so the popup matches. */
|
|
625
|
-
size?: SizeVariant
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
type ComboboxInputProps = ComboboxFieldProps
|
|
629
|
-
|
|
630
|
-
// Icon swap (motion.md): the chevron and the spinner share one cell.
|
|
631
|
-
const SHOWN = { opacity: 1, scale: 1, filter: 'blur(0px)' }
|
|
632
|
-
const HIDDEN = { opacity: 0, scale: 0.6, filter: 'blur(4px)' }
|
|
633
|
-
const swapEnter = { type: 'tween' as const, duration: spring.fast.duration, ease: 'easeOut' as const }
|
|
634
|
-
const swapLeave = { type: 'tween' as const, ...spring.fast.exit, ease: 'easeIn' as const }
|
|
635
|
-
|
|
636
|
-
/** The trailing clear ✕ and chevron, shared by both fields. */
|
|
637
|
-
function FieldControls({
|
|
638
|
-
clearable,
|
|
639
|
-
hideTrigger,
|
|
640
|
-
compact,
|
|
641
|
-
iconSize,
|
|
642
|
-
}: {
|
|
643
|
-
clearable: boolean
|
|
644
|
-
hideTrigger: boolean
|
|
645
|
-
compact: boolean
|
|
646
|
-
iconSize: number
|
|
647
|
-
}) {
|
|
648
|
-
const XIcon = useIcon('x')
|
|
649
|
-
const Loader = useIcon('loader-circle')
|
|
650
|
-
const pill = useShape().variant === 'pill'
|
|
651
|
-
const { loading, readOnly, labels } = useComboboxContext()
|
|
652
|
-
const reduced = useReduceMotion()
|
|
653
|
-
const swap = (shown: boolean) => (reduced ? { opacity: shown ? 1 : 0 } : shown ? SHOWN : HIDDEN)
|
|
654
|
-
return (
|
|
655
|
-
<>
|
|
656
|
-
{clearable && !readOnly && (
|
|
657
|
-
// The primitive hides the button while there is nothing to clear; a
|
|
658
|
-
// fixed slot keeps the field's width steady either way.
|
|
659
|
-
<span className={cn('flex shrink-0 items-center justify-center', compact ? 'size-5' : 'size-6')}>
|
|
660
|
-
<ComboboxPrimitive.Clear
|
|
661
|
-
aria-label={labels.clear ?? 'Clear'}
|
|
662
|
-
className={cn(clearButtonClass, pill && 'rounded-full', compact ? 'size-5' : 'size-6')}
|
|
663
|
-
>
|
|
664
|
-
<XIcon size={iconSize} strokeWidth={1.5} />
|
|
665
|
-
</ComboboxPrimitive.Clear>
|
|
666
|
-
</span>
|
|
667
|
-
)}
|
|
668
|
-
{/* Not a tab stop: the field itself opens on ArrowDown or typing. */}
|
|
669
|
-
{!hideTrigger && (
|
|
670
|
-
<ComboboxPrimitive.Trigger
|
|
671
|
-
aria-label={labels.open ?? 'Open'}
|
|
672
|
-
tabIndex={-1}
|
|
673
|
-
className={cn(fieldButtonClass, compact ? 'size-5' : 'size-6')}
|
|
674
|
-
>
|
|
675
|
-
{/* While loading, the chevron blurs into a spinner in the same cell. */}
|
|
676
|
-
<span className="grid" style={{ width: iconSize, height: iconSize }}>
|
|
677
|
-
<motion.span
|
|
678
|
-
className="col-start-1 row-start-1 flex"
|
|
679
|
-
initial={false}
|
|
680
|
-
animate={swap(!loading)}
|
|
681
|
-
transition={loading ? swapLeave : swapEnter}
|
|
682
|
-
>
|
|
683
|
-
<svg
|
|
684
|
-
width={iconSize}
|
|
685
|
-
height={iconSize}
|
|
686
|
-
viewBox="0 0 24 24"
|
|
687
|
-
fill="none"
|
|
688
|
-
stroke="currentColor"
|
|
689
|
-
strokeWidth={2}
|
|
690
|
-
strokeLinecap="round"
|
|
691
|
-
strokeLinejoin="round"
|
|
692
|
-
className={cn('transition-colors duration-fast', readOnly && 'invisible')}
|
|
693
|
-
>
|
|
694
|
-
<path d="M6 9l6 6 6-6" />
|
|
695
|
-
</svg>
|
|
696
|
-
</motion.span>
|
|
697
|
-
<motion.span
|
|
698
|
-
aria-hidden
|
|
699
|
-
className="col-start-1 row-start-1 flex"
|
|
700
|
-
initial={false}
|
|
701
|
-
animate={swap(loading)}
|
|
702
|
-
transition={loading ? swapEnter : swapLeave}
|
|
703
|
-
>
|
|
704
|
-
<Loader size={iconSize} strokeWidth={1.5} className="animate-spin motion-reduce:animate-none" />
|
|
705
|
-
</motion.span>
|
|
706
|
-
</span>
|
|
707
|
-
</ComboboxPrimitive.Trigger>
|
|
708
|
-
)}
|
|
709
|
-
</>
|
|
710
|
-
)
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
const ComboboxInput = forwardRef<HTMLInputElement, ComboboxInputProps>(
|
|
714
|
-
(
|
|
715
|
-
{
|
|
716
|
-
className,
|
|
717
|
-
variant = 'bordered',
|
|
718
|
-
icon: Icon,
|
|
719
|
-
placeholder,
|
|
720
|
-
error,
|
|
721
|
-
invalid,
|
|
722
|
-
hideTrigger = false,
|
|
723
|
-
clearable = false,
|
|
724
|
-
size,
|
|
725
|
-
...props
|
|
726
|
-
},
|
|
727
|
-
ref,
|
|
728
|
-
) => {
|
|
729
|
-
const { anchorRef, readOnly, labels } = useComboboxContext()
|
|
730
|
-
// `error` forces the destructive state; otherwise the prop, then a surrounding Field's.
|
|
731
|
-
const box = useFieldBox({ variant, invalid: error ? true : invalid, readOnly, size })
|
|
732
|
-
const { size: sizeClasses, props: boxProps } = box
|
|
733
|
-
const compact = sizeClasses.variant === 'compact'
|
|
734
|
-
|
|
735
|
-
return (
|
|
736
|
-
<div className="flex flex-col gap-1">
|
|
737
|
-
{/* The group is the popup's anchor, so the list spans the whole
|
|
738
|
-
field (icon to chevron), not just the text input. */}
|
|
739
|
-
<ComboboxPrimitive.InputGroup
|
|
740
|
-
ref={anchorRef}
|
|
741
|
-
{...boxProps}
|
|
742
|
-
className={cn(
|
|
743
|
-
boxProps.className,
|
|
744
|
-
'flex items-center',
|
|
745
|
-
sizeClasses.control,
|
|
746
|
-
sizeClasses.gap,
|
|
747
|
-
compact ? 'min-w-[128px]' : 'min-w-[160px]',
|
|
748
|
-
className,
|
|
749
|
-
)}
|
|
750
|
-
>
|
|
751
|
-
{Icon && <Icon size={sizeClasses.icon} strokeWidth={1.5} className={fieldIconClasses} />}
|
|
752
|
-
<ComboboxPrimitive.Input
|
|
753
|
-
ref={ref}
|
|
754
|
-
placeholder={placeholder ?? labels.placeholder ?? 'Search…'}
|
|
755
|
-
aria-invalid={box.invalid || undefined}
|
|
756
|
-
className={cn(
|
|
757
|
-
'min-w-0 flex-1 rounded-none bg-transparent text-foreground placeholder:text-muted-foreground outline-none font-[inherit]',
|
|
758
|
-
sizeClasses.text,
|
|
759
|
-
// The caret is as tall as the line box (Chrome, Safari); the
|
|
760
|
-
// ladder's leading keeps it in proportion to the field.
|
|
761
|
-
compact ? 'leading-5' : 'leading-6',
|
|
762
|
-
)}
|
|
763
|
-
{...props}
|
|
764
|
-
/>
|
|
765
|
-
<FieldControls
|
|
766
|
-
clearable={clearable}
|
|
767
|
-
hideTrigger={hideTrigger}
|
|
768
|
-
compact={compact}
|
|
769
|
-
iconSize={sizeClasses.icon}
|
|
770
|
-
/>
|
|
771
|
-
</ComboboxPrimitive.InputGroup>
|
|
772
|
-
{error && <span className="text-caption text-destructive pl-3">{error}</span>}
|
|
773
|
-
</div>
|
|
774
|
-
)
|
|
775
|
-
},
|
|
776
|
-
)
|
|
777
|
-
|
|
778
|
-
ComboboxInput.displayName = 'ComboboxInput'
|
|
779
|
-
|
|
780
|
-
// ---------------------------------------------------------------------------
|
|
781
|
-
// ComboboxChips — the multiple-selection field: one chip per selected item
|
|
782
|
-
// ahead of the text input, wrapping onto new lines as they accumulate.
|
|
783
|
-
// Backspace in an empty input removes the last chip; ArrowLeft walks into
|
|
784
|
-
// the chips, where Backspace/Delete removes the focused one.
|
|
785
|
-
// ---------------------------------------------------------------------------
|
|
786
|
-
|
|
787
|
-
type ComboboxChipsProps = ComboboxFieldProps
|
|
788
|
-
|
|
789
|
-
/** Springs the chips field's height to the measured chip row stack (plus
|
|
790
|
-
* `padY`, the field's vertical padding) as chips wrap and unwrap. The
|
|
791
|
-
* height lives in a MotionValue driving an inline style, and for a reason:
|
|
792
|
-
* the field must stay pinned at its old height through the reflow that
|
|
793
|
-
* wraps a chip, or its auto height lands on the new row before any spring
|
|
794
|
-
* can start and the growth snaps. First measure sets the pin; every later
|
|
795
|
-
* one springs it. Measured (offsetHeight + ResizeObserver), never
|
|
796
|
-
* `height: "auto"`: framer resolves "auto" from the element's *visual*
|
|
797
|
-
* (transformed) size, so under a scaled ancestor (the /demo card) the
|
|
798
|
-
* spring would overshoot. Layout metrics are transform-immune. */
|
|
799
|
-
function useChipRowHeight(padY: number) {
|
|
800
|
-
const height = useMotionValue<number | 'auto'>('auto')
|
|
801
|
-
const roRef = useRef<ResizeObserver | null>(null)
|
|
802
|
-
const padRef = useRef(padY)
|
|
803
|
-
padRef.current = padY
|
|
804
|
-
const ref = useCallback(
|
|
805
|
-
(el: HTMLDivElement | null) => {
|
|
806
|
-
roRef.current?.disconnect()
|
|
807
|
-
roRef.current = null
|
|
808
|
-
if (!el) return
|
|
809
|
-
const measure = () => {
|
|
810
|
-
if (el.offsetHeight <= 0) return
|
|
811
|
-
const target = el.offsetHeight + padRef.current
|
|
812
|
-
if (height.get() === 'auto') height.set(target)
|
|
813
|
-
else if (height.get() !== target) animate(height, target, spring.fast)
|
|
814
|
-
}
|
|
815
|
-
measure()
|
|
816
|
-
const ro = new ResizeObserver(measure)
|
|
817
|
-
ro.observe(el)
|
|
818
|
-
roRef.current = ro
|
|
819
|
-
},
|
|
820
|
-
[height],
|
|
821
|
-
)
|
|
822
|
-
return { ref, height }
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
const ComboboxChips = forwardRef<HTMLInputElement, ComboboxChipsProps>(
|
|
826
|
-
(
|
|
827
|
-
{
|
|
828
|
-
className,
|
|
829
|
-
variant = 'bordered',
|
|
830
|
-
icon: Icon,
|
|
831
|
-
placeholder,
|
|
832
|
-
error,
|
|
833
|
-
invalid,
|
|
834
|
-
hideTrigger = false,
|
|
835
|
-
clearable = false,
|
|
836
|
-
size,
|
|
837
|
-
...props
|
|
838
|
-
},
|
|
839
|
-
ref,
|
|
840
|
-
) => {
|
|
841
|
-
const XIcon = useIcon('x')
|
|
842
|
-
const shape = useShape()
|
|
843
|
-
const { anchorRef, open, disabled, inputValue, values, readOnly, labels } = useComboboxContext()
|
|
844
|
-
const box = useFieldBox({ variant, invalid: error ? true : invalid, disabled, readOnly, size })
|
|
845
|
-
const { size: sizeClasses, props: boxProps } = box
|
|
846
|
-
const compact = sizeClasses.variant === 'compact'
|
|
847
|
-
const rowHeight = useChipRowHeight(compact ? 8 : 12)
|
|
848
|
-
|
|
849
|
-
return (
|
|
850
|
-
<div className="flex flex-col gap-1">
|
|
851
|
-
<ComboboxPrimitive.Chips
|
|
852
|
-
ref={anchorRef}
|
|
853
|
-
// The field springs to the chip rows' measured height as they
|
|
854
|
-
// wrap and unwrap, instead of snapping a row taller or shorter.
|
|
855
|
-
render={<motion.div style={{ height: rowHeight.height }} />}
|
|
856
|
-
// Chips stamps no state attributes of its own (InputGroup does);
|
|
857
|
-
// the field ladder (`lib/field-classes.ts`) and the chevron read these.
|
|
858
|
-
{...boxProps}
|
|
859
|
-
data-popup-open={open || undefined}
|
|
860
|
-
className={cn(
|
|
861
|
-
boxProps.className,
|
|
862
|
-
'flex items-center',
|
|
863
|
-
// The spring above owns the height; leaving it in transition-all
|
|
864
|
-
// would have the 80ms CSS ease drag behind every spring frame.
|
|
865
|
-
'transition-[color,background-color,border-radius,box-shadow,opacity]',
|
|
866
|
-
// Grows with its chips. Rows top-align (`items-start`) so the
|
|
867
|
-
// icon and controls hold the first line while chips wrap; the
|
|
868
|
-
// vertical padding is exactly what centers one 24px (20px
|
|
869
|
-
// compact) row inside the ladder height.
|
|
870
|
-
'!items-start',
|
|
871
|
-
compact ? 'min-h-7 py-1' : 'min-h-9 py-1.5',
|
|
872
|
-
sizeClasses.gap,
|
|
873
|
-
compact ? 'min-w-[128px]' : 'min-w-[160px]',
|
|
874
|
-
className,
|
|
875
|
-
)}
|
|
876
|
-
>
|
|
877
|
-
{Icon && (
|
|
878
|
-
// A row-height box keeps the icon centered on the first line.
|
|
879
|
-
<span className={cn('flex shrink-0 items-center', compact ? 'h-5' : 'h-6')}>
|
|
880
|
-
<Icon size={sizeClasses.icon} strokeWidth={1.5} className={fieldIconClasses} />
|
|
881
|
-
</span>
|
|
882
|
-
)}
|
|
883
|
-
<div
|
|
884
|
-
ref={rowHeight.ref}
|
|
885
|
-
className={cn(
|
|
886
|
-
'relative flex min-w-0 flex-1 flex-wrap items-center gap-1',
|
|
887
|
-
// A chip leads the field: the row pulls left so the chip sits
|
|
888
|
-
// an even distance from every edge. The offset lives here, not
|
|
889
|
-
// on the container's padding — the container's transition-all
|
|
890
|
-
// would ease a padding change against the chip's spring and
|
|
891
|
-
// drag the caret with it; the row snaps instead, in the same
|
|
892
|
-
// frame as the chip. An icon keeps the full inset — it leads,
|
|
893
|
-
// not the chip.
|
|
894
|
-
!Icon && values.length > 0 && '-ml-1',
|
|
895
|
-
)}
|
|
896
|
-
>
|
|
897
|
-
<ComboboxPrimitive.Value>
|
|
898
|
-
{(selected: ComboboxItemData[] | null) => (
|
|
899
|
-
<>
|
|
900
|
-
{/* Chips pop in and out on the fast tier and slide into
|
|
901
|
-
their new slots; an exiting chip is inert while it
|
|
902
|
-
fades, since the primitive has already dropped it.
|
|
903
|
-
popLayout lifts the exiting chip out of the flow at
|
|
904
|
-
once, so the field reflows immediately rather than
|
|
905
|
-
after the fade. */}
|
|
906
|
-
<AnimatePresence initial={false} mode="popLayout">
|
|
907
|
-
{(selected ?? []).map(item => {
|
|
908
|
-
const label = itemLabel(item)
|
|
909
|
-
return (
|
|
910
|
-
<motion.span
|
|
911
|
-
key={itemValue(item)}
|
|
912
|
-
layout
|
|
913
|
-
initial={{ opacity: 0, scale: 0.9 }}
|
|
914
|
-
animate={{ opacity: 1, scale: 1 }}
|
|
915
|
-
exit={{
|
|
916
|
-
opacity: 0,
|
|
917
|
-
scale: 0.9,
|
|
918
|
-
pointerEvents: 'none',
|
|
919
|
-
transition: spring.fast.exit,
|
|
920
|
-
}}
|
|
921
|
-
transition={spring.fast}
|
|
922
|
-
// Grow from the left edge, where the typed text
|
|
923
|
-
// just was — not from the middle of the chip.
|
|
924
|
-
style={{ originX: 0 }}
|
|
925
|
-
className="inline-flex max-w-full shrink-0"
|
|
926
|
-
>
|
|
927
|
-
<ComboboxPrimitive.Chip
|
|
928
|
-
aria-label={label}
|
|
929
|
-
className={cn(
|
|
930
|
-
'inline-flex max-w-full shrink-0 items-center gap-0.5 bg-hover pl-2 pr-0.5 text-foreground outline-none',
|
|
931
|
-
shape.variant === 'pill' ? 'rounded-full' : 'rounded-md',
|
|
932
|
-
'focus-visible:ring-1 focus-visible:ring-focus-ring',
|
|
933
|
-
compact ? 'h-5 text-caption-compact' : 'h-6 text-caption',
|
|
934
|
-
)}
|
|
935
|
-
>
|
|
936
|
-
<span
|
|
937
|
-
className={cn(
|
|
938
|
-
typeof itemNode(item) === 'string' ? 'truncate' : 'flex min-w-0 items-center',
|
|
939
|
-
readOnly && 'pr-1.5',
|
|
940
|
-
)}
|
|
941
|
-
>
|
|
942
|
-
{itemNode(item)}
|
|
943
|
-
</span>
|
|
944
|
-
{!readOnly && (
|
|
945
|
-
<ComboboxPrimitive.ChipRemove
|
|
946
|
-
aria-label={labels.remove ? labels.remove(label) : `Remove ${label}`}
|
|
947
|
-
className={cn(
|
|
948
|
-
chipRemoveClass,
|
|
949
|
-
shape.variant === 'pill' && 'rounded-full',
|
|
950
|
-
compact ? 'size-4' : 'size-5',
|
|
951
|
-
)}
|
|
952
|
-
>
|
|
953
|
-
<XIcon size={compact ? 10 : 12} strokeWidth={2} />
|
|
954
|
-
</ComboboxPrimitive.ChipRemove>
|
|
955
|
-
)}
|
|
956
|
-
</ComboboxPrimitive.Chip>
|
|
957
|
-
</motion.span>
|
|
958
|
-
)
|
|
959
|
-
})}
|
|
960
|
-
</AnimatePresence>
|
|
961
|
-
{/* The field sizes to what is typed (`size` is the
|
|
962
|
-
intrinsic width; flex-auto grows it across the rest
|
|
963
|
-
of its row) so it stays beside the chips while it
|
|
964
|
-
fits and wraps only once the text no longer does.
|
|
965
|
-
It never animates: chips slide, the field snaps.
|
|
966
|
-
Animating it read as the placeholder sliding in from
|
|
967
|
-
the right when the last chip went. */}
|
|
968
|
-
<span className="flex min-w-6 flex-auto">
|
|
969
|
-
<ComboboxPrimitive.Input
|
|
970
|
-
ref={ref}
|
|
971
|
-
size={Math.max(1, inputValue.length + 1)}
|
|
972
|
-
placeholder={selected?.length ? undefined : (placeholder ?? labels.placeholder ?? 'Search…')}
|
|
973
|
-
aria-invalid={box.invalid || undefined}
|
|
974
|
-
className={cn(
|
|
975
|
-
'w-full min-w-0 rounded-none bg-transparent text-foreground placeholder:text-muted-foreground outline-none font-[inherit]',
|
|
976
|
-
// Line box = row height, so the caret spans the chip row.
|
|
977
|
-
compact ? 'h-5 leading-5' : 'h-6 leading-6',
|
|
978
|
-
sizeClasses.text,
|
|
979
|
-
)}
|
|
980
|
-
{...props}
|
|
981
|
-
/>
|
|
982
|
-
</span>
|
|
983
|
-
</>
|
|
984
|
-
)}
|
|
985
|
-
</ComboboxPrimitive.Value>
|
|
986
|
-
</div>
|
|
987
|
-
<FieldControls
|
|
988
|
-
clearable={clearable}
|
|
989
|
-
hideTrigger={hideTrigger}
|
|
990
|
-
compact={compact}
|
|
991
|
-
iconSize={sizeClasses.icon}
|
|
992
|
-
/>
|
|
993
|
-
</ComboboxPrimitive.Chips>
|
|
994
|
-
{error && <span className="text-caption text-destructive pl-3">{error}</span>}
|
|
995
|
-
</div>
|
|
996
|
-
)
|
|
997
|
-
},
|
|
998
|
-
)
|
|
999
|
-
|
|
1000
|
-
ComboboxChips.displayName = 'ComboboxChips'
|
|
1001
|
-
|
|
1002
|
-
// ---------------------------------------------------------------------------
|
|
1003
|
-
// ComboboxContent — the popup surface. Holds ComboboxEmpty and ComboboxList.
|
|
1004
|
-
// ---------------------------------------------------------------------------
|
|
1005
|
-
|
|
1006
|
-
interface ComboboxContentProps {
|
|
1007
|
-
/** Classes for the popup surface — merged after the radius and max-height. */
|
|
1008
|
-
className?: string
|
|
1009
|
-
/** `Combobox.Empty` and `Combobox.List`. */
|
|
1010
|
-
children: ReactNode
|
|
1011
|
-
/** Side of the field the popup prefers. Defaults to `"bottom"`. */
|
|
1012
|
-
side?: 'top' | 'bottom' | 'left' | 'right'
|
|
1013
|
-
/** Alignment against the field. Defaults to `"start"`. */
|
|
1014
|
-
align?: 'start' | 'center' | 'end'
|
|
1015
|
-
/** Gap to the field, in px. Ignored when the list opens in place over the field (`effect="morph"` with `hideSource`). Defaults to `6`. */
|
|
1016
|
-
sideOffset?: number
|
|
1017
|
-
/** Where the list grows from (see Morph): the field, the press point, its own center, a viewport edge, or a ref to any element. Defaults to `'trigger'` (the field). */
|
|
1018
|
-
from?: 'trigger' | 'pointer' | 'center' | 'top' | 'right' | 'bottom' | 'left' | RefObject<HTMLElement | null>
|
|
1019
|
-
/** How it grows (see Morph): with the liquid goo neck, a plain morph, a slide or a fade. Defaults to `'goo'`. */
|
|
1020
|
-
effect?: 'goo' | 'morph' | 'slide' | 'fade'
|
|
1021
|
-
/** Hide the field while open, so it reads as turning into the list. Defaults to `false`. */
|
|
1022
|
-
hideSource?: boolean
|
|
1023
|
-
/** Spring tier of a plain morph, slide or fade; goo runs on its own `spring.goo`. Defaults to `'moderate'`. */
|
|
1024
|
-
tier?: 'moderate' | 'slow'
|
|
1025
|
-
/** Pinned under the list, past a hairline: a "load more" or "add new" action. A function gets `{ close, query }`. */
|
|
1026
|
-
footer?: ComboboxFooter
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
const ComboboxContent = forwardRef<HTMLDivElement, ComboboxContentProps>(
|
|
1030
|
-
(
|
|
1031
|
-
{
|
|
1032
|
-
className,
|
|
1033
|
-
children,
|
|
1034
|
-
side = 'bottom',
|
|
1035
|
-
align = 'start',
|
|
1036
|
-
sideOffset = 6,
|
|
1037
|
-
from,
|
|
1038
|
-
effect,
|
|
1039
|
-
hideSource,
|
|
1040
|
-
tier = 'moderate',
|
|
1041
|
-
footer,
|
|
1042
|
-
},
|
|
1043
|
-
ref,
|
|
1044
|
-
) => {
|
|
1045
|
-
const { open, origin, anchorRef, setOpen, inputValue } = useComboboxContext()
|
|
1046
|
-
const morph = useMorph(open, origin, { from, effect, hideSource, tier })
|
|
1047
|
-
useImperativeHandle(ref, () => morph.popup as HTMLDivElement, [morph.popup])
|
|
1048
|
-
// Lifts 2 levels off its substrate with a fixed shadow (see Elevated).
|
|
1049
|
-
const level = Math.min(useSurface() + 2, 8)
|
|
1050
|
-
const shape = popupShape
|
|
1051
|
-
|
|
1052
|
-
return (
|
|
1053
|
-
<ComboboxPrimitive.Portal>
|
|
1054
|
-
<ComboboxPrimitive.Positioner
|
|
1055
|
-
// The whole field (input group or chips container) is the anchor,
|
|
1056
|
-
// so the list spans it edge to edge.
|
|
1057
|
-
anchor={anchorRef}
|
|
1058
|
-
side={side}
|
|
1059
|
-
align={align}
|
|
1060
|
-
sideOffset={opensInPlace({ effect, hideSource }) ? inPlaceOffset : sideOffset}
|
|
1061
|
-
className="z-50 outline-none"
|
|
1062
|
-
>
|
|
1063
|
-
<ComboboxPrimitive.Popup ref={morph.popupRef} className="relative select-none outline-none">
|
|
1064
|
-
<SurfaceProvider value={level}>
|
|
1065
|
-
<MorphSurface
|
|
1066
|
-
morph={morph}
|
|
1067
|
-
bg={SURFACE_BG[level]}
|
|
1068
|
-
shadow={SURFACE_SHADOW[3]}
|
|
1069
|
-
radius={shape.container}
|
|
1070
|
-
className={cn(
|
|
1071
|
-
// min-w tracks the field via the Positioner's --anchor-width
|
|
1072
|
-
// var; the list inside owns padding and scrolling.
|
|
1073
|
-
`flex flex-col min-w-[var(--anchor-width)] max-h-[min(300px,var(--available-height))] overflow-hidden ${shape.container}`,
|
|
1074
|
-
className,
|
|
1075
|
-
)}
|
|
1076
|
-
>
|
|
1077
|
-
{children}
|
|
1078
|
-
{footer !== undefined && (
|
|
1079
|
-
// Outside the scrolling list, so it stays put while the rows scroll.
|
|
1080
|
-
<div className="shrink-0 border-t border-border p-1">
|
|
1081
|
-
{typeof footer === 'function'
|
|
1082
|
-
? footer({ close: () => setOpen(false), query: inputValue.trim() })
|
|
1083
|
-
: footer}
|
|
1084
|
-
</div>
|
|
1085
|
-
)}
|
|
1086
|
-
</MorphSurface>
|
|
1087
|
-
</SurfaceProvider>
|
|
1088
|
-
</ComboboxPrimitive.Popup>
|
|
1089
|
-
</ComboboxPrimitive.Positioner>
|
|
1090
|
-
</ComboboxPrimitive.Portal>
|
|
1091
|
-
)
|
|
1092
|
-
},
|
|
1093
|
-
)
|
|
1094
|
-
|
|
1095
|
-
ComboboxContent.displayName = 'ComboboxContent'
|
|
1096
|
-
|
|
1097
|
-
// ---------------------------------------------------------------------------
|
|
1098
|
-
// ComboboxList — renders a row per matching item, carrying the fluid hover
|
|
1099
|
-
// hover overlays and the animated selected background / focus ring.
|
|
1100
|
-
// ---------------------------------------------------------------------------
|
|
1101
|
-
|
|
1102
|
-
interface ComboboxListProps {
|
|
1103
|
-
/** Classes for the rows' container. */
|
|
1104
|
-
className?: string
|
|
1105
|
-
/** Render one row per matching item: `(item, index) => <Combobox.Item …/>`. Rows get their index from the list. */
|
|
1106
|
-
children: (item: ComboboxItemData, index: number) => ReactNode
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
const ComboboxList = forwardRef<HTMLDivElement, ComboboxListProps>(({ className, children }, ref) => {
|
|
1110
|
-
const { open, values, multiple, inputValue, createRow, loading, loadingLabel } = useComboboxContext()
|
|
1111
|
-
const Loader = useIcon('loader-circle')
|
|
1112
|
-
const sizeClasses = useSize()
|
|
1113
|
-
const highlight = useContext(ComboboxHighlightContext)
|
|
1114
|
-
const PlusIcon = useIcon('plus')
|
|
1115
|
-
const shape = popupShape
|
|
1116
|
-
const containerRef = useRef<HTMLDivElement>(null)
|
|
1117
|
-
|
|
1118
|
-
const hover = useFluidHover(containerRef, { isItemDisabled: isDisabledRow })
|
|
1119
|
-
const { activeIndex, setActiveIndex, itemRects, isMeasured, handlers, registerItem, remeasure } = hover
|
|
1120
|
-
|
|
1121
|
-
// Checked rows by index — one in single mode, any number in multiple.
|
|
1122
|
-
const [checkedIndices, setCheckedIndices] = useState<number[]>([])
|
|
1123
|
-
|
|
1124
|
-
// Fresh rects once per open — the popup keeps its rows registered while
|
|
1125
|
-
// it sits hidden between opens, so registration alone never triggers a
|
|
1126
|
-
// pass on reopen. Filtering re-registers rows, which the hook coalesces
|
|
1127
|
-
// into its own measurement.
|
|
1128
|
-
useEffect(() => {
|
|
1129
|
-
if (!open) return
|
|
1130
|
-
remeasure()
|
|
1131
|
-
}, [open, remeasure])
|
|
1132
|
-
|
|
1133
|
-
// Typing re-filters the rows underneath the overlays, and those frames
|
|
1134
|
-
// must snap, not glide — an overlay springing to wherever its row LANDED
|
|
1135
|
-
// reads as the list smearing on every keystroke. The primitive filters
|
|
1136
|
-
// internally, so the query is the reflow signal (picks don't change it,
|
|
1137
|
-
// keeping multiple mode's merge/split glide). The flag arms on the
|
|
1138
|
-
// query's render and holds until the rects measured from the new row
|
|
1139
|
-
// set land — measurement coalesces on a rAF, one render behind.
|
|
1140
|
-
const reflowRef = useRef(false)
|
|
1141
|
-
const prevQueryRef = useRef(inputValue)
|
|
1142
|
-
const armedRectsRef = useRef(itemRects)
|
|
1143
|
-
if (prevQueryRef.current !== inputValue) {
|
|
1144
|
-
prevQueryRef.current = inputValue
|
|
1145
|
-
armedRectsRef.current = itemRects
|
|
1146
|
-
reflowRef.current = true
|
|
1147
|
-
}
|
|
1148
|
-
const reflowSnap = reflowRef.current
|
|
1149
|
-
useEffect(() => {
|
|
1150
|
-
if (reflowRef.current && itemRects !== armedRectsRef.current) reflowRef.current = false
|
|
1151
|
-
})
|
|
1152
|
-
|
|
1153
|
-
// Detect the checked rows. Their indices shift as the query filters the
|
|
1154
|
-
// list, so the typed value is a dependency too.
|
|
1155
|
-
useEffect(() => {
|
|
1156
|
-
if (!open) return
|
|
1157
|
-
// Double rAF: first waits for React commit, second for layout
|
|
1158
|
-
let inner: number
|
|
1159
|
-
const outer = requestAnimationFrame(() => {
|
|
1160
|
-
inner = requestAnimationFrame(() => {
|
|
1161
|
-
const container = containerRef.current
|
|
1162
|
-
if (container) {
|
|
1163
|
-
const rows = Array.from(container.querySelectorAll('[data-fluid-hover-index]')) as HTMLElement[]
|
|
1164
|
-
const next: number[] = []
|
|
1165
|
-
rows.forEach((el, i) => {
|
|
1166
|
-
if (values.includes(el.getAttribute('data-value') ?? '')) next.push(i)
|
|
1167
|
-
})
|
|
1168
|
-
setCheckedIndices(next)
|
|
1169
|
-
}
|
|
1170
|
-
})
|
|
1171
|
-
})
|
|
1172
|
-
return () => {
|
|
1173
|
-
cancelAnimationFrame(outer)
|
|
1174
|
-
cancelAnimationFrame(inner)
|
|
1175
|
-
}
|
|
1176
|
-
}, [open, values, inputValue])
|
|
1177
|
-
|
|
1178
|
-
// A keyboard (or auto) highlight drives the hover background — the only
|
|
1179
|
-
// indicator: the row Enter would pick reads like the row the pointer
|
|
1180
|
-
// would pick, with no ring.
|
|
1181
|
-
useEffect(() => {
|
|
1182
|
-
// A dropped highlight (the input is the stop again) takes the hover
|
|
1183
|
-
// background with it — otherwise it would linger on the last row.
|
|
1184
|
-
if (!highlight) setActiveIndex(null)
|
|
1185
|
-
else if (highlight.keyboard) setActiveIndex(highlight.index)
|
|
1186
|
-
}, [highlight, setActiveIndex])
|
|
1187
|
-
|
|
1188
|
-
// Reset every overlay index as the close begins, so the reopen doesn't
|
|
1189
|
-
// spring an overlay from a stale row.
|
|
1190
|
-
useEffect(() => {
|
|
1191
|
-
if (open) return
|
|
1192
|
-
setCheckedIndices([])
|
|
1193
|
-
setActiveIndex(null)
|
|
1194
|
-
}, [open, setActiveIndex])
|
|
1195
|
-
|
|
1196
|
-
// Overlays read rects only once the hook reports the row set fully
|
|
1197
|
-
// measured — positioning one from an incomplete pass mounts it at the
|
|
1198
|
-
// wrong row, and the correcting pass then springs it across the list.
|
|
1199
|
-
// Single mode glides ONE marker between rows (a value change springs it
|
|
1200
|
-
// to the picked row). Multiple mode paints one block per contiguous run
|
|
1201
|
-
// of checked rows — merging and splitting like Checkbox.Group as picks
|
|
1202
|
-
// bridge or break a run.
|
|
1203
|
-
const firstChecked = checkedIndices[0]
|
|
1204
|
-
const checkedRect = isMeasured && !multiple && firstChecked !== undefined ? (itemRects[firstChecked] ?? null) : null
|
|
1205
|
-
const runs = useSelectionRuns(multiple ? checkedIndices : [])
|
|
1206
|
-
const blocks = useMergeSplitBlocks(runs, isMeasured && open ? itemRects : [], shape.bgRadius)
|
|
1207
|
-
|
|
1208
|
-
const contentCtx = useMemo(() => ({ registerItem, activeIndex }), [registerItem, activeIndex])
|
|
1209
|
-
|
|
1210
|
-
return (
|
|
1211
|
-
<ComboboxContentContext.Provider value={contentCtx}>
|
|
1212
|
-
<ScrollArea className={popupScrollAreaClass} viewportClassName={cn(popupViewportClass, 'scroll-fade')}>
|
|
1213
|
-
<ComboboxPrimitive.List
|
|
1214
|
-
ref={(node: HTMLDivElement | null) => {
|
|
1215
|
-
;(containerRef as MutableRefObject<HTMLDivElement | null>).current = node
|
|
1216
|
-
if (typeof ref === 'function') ref(node)
|
|
1217
|
-
else if (ref) (ref as MutableRefObject<HTMLDivElement | null>).current = node
|
|
1218
|
-
}}
|
|
1219
|
-
onMouseEnter={handlers.onMouseEnter}
|
|
1220
|
-
onMouseMove={handlers.onMouseMove}
|
|
1221
|
-
onMouseLeave={handlers.onMouseLeave}
|
|
1222
|
-
onClick={handlers.onClick}
|
|
1223
|
-
className={cn(
|
|
1224
|
-
// The list is the overlays' offsetParent, so rows and overlays
|
|
1225
|
-
// scroll together inside the ScrollArea. Padding collapses when
|
|
1226
|
-
// the list is empty (ComboboxEmpty takes over).
|
|
1227
|
-
'relative flex flex-col p-1 outline-none data-[empty]:p-0',
|
|
1228
|
-
className,
|
|
1229
|
-
)}
|
|
1230
|
-
>
|
|
1231
|
-
{/* The three overlays are torn down as the close begins rather
|
|
1232
|
-
than exit-animated: an overlay still mounted when the popup
|
|
1233
|
-
reopens is one AnimatePresence re-adopts under its old key and
|
|
1234
|
-
animates from the row it had before. */}
|
|
1235
|
-
{/* Selected background */}
|
|
1236
|
-
{open && multiple && (
|
|
1237
|
-
<SelectionBackgrounds blocks={reflowSnap ? blocks.map(b => ({ ...b, instant: true })) : blocks} />
|
|
1238
|
-
)}
|
|
1239
|
-
{open && !multiple && (
|
|
1240
|
-
<AnimatePresence>
|
|
1241
|
-
{checkedRect && (
|
|
1242
|
-
<motion.div
|
|
1243
|
-
key="checked"
|
|
1244
|
-
aria-hidden
|
|
1245
|
-
className={`absolute ${shape.bg} bg-active pointer-events-none`}
|
|
1246
|
-
initial={false}
|
|
1247
|
-
animate={{
|
|
1248
|
-
top: checkedRect.top,
|
|
1249
|
-
left: checkedRect.left,
|
|
1250
|
-
width: checkedRect.width,
|
|
1251
|
-
height: checkedRect.height,
|
|
1252
|
-
opacity: 1,
|
|
1253
|
-
}}
|
|
1254
|
-
exit={{ opacity: 0, transition: spring.moderate.exit }}
|
|
1255
|
-
transition={
|
|
1256
|
-
reflowSnap ? { duration: 0 } : { ...spring.moderate, opacity: { duration: spring.fast.duration } }
|
|
1257
|
-
}
|
|
1258
|
-
/>
|
|
1259
|
-
)}
|
|
1260
|
-
</AnimatePresence>
|
|
1261
|
-
)}
|
|
1262
|
-
|
|
1263
|
-
{/* Hover background. Snaps (no travel) while a filter reflow is
|
|
1264
|
-
moving the rows underneath. */}
|
|
1265
|
-
{open && (
|
|
1266
|
-
<FluidHoverHighlight hover={hover} className={shape.bg} transition={reflowSnap ? false : undefined} />
|
|
1267
|
-
)}
|
|
1268
|
-
|
|
1269
|
-
{/* The overlays above are plain children, so the rows come from
|
|
1270
|
-
an explicit Collection (a function child on List would have
|
|
1271
|
-
to be its only child). */}
|
|
1272
|
-
<ComboboxPrimitive.Collection>
|
|
1273
|
-
{(item: ComboboxItemData, index: number) => (
|
|
1274
|
-
<ComboboxItemIndexContext.Provider key={itemValue(item)} value={index}>
|
|
1275
|
-
{/* The create row is the list's, not the consumer's: it
|
|
1276
|
-
reads the label from the root and picks like any row. */}
|
|
1277
|
-
{isCreateItem(item) ? (
|
|
1278
|
-
<ComboboxItem value={CREATE_VALUE} icon={PlusIcon}>
|
|
1279
|
-
{createRow}
|
|
1280
|
-
</ComboboxItem>
|
|
1281
|
-
) : (
|
|
1282
|
-
children(item, index)
|
|
1283
|
-
)}
|
|
1284
|
-
</ComboboxItemIndexContext.Provider>
|
|
1285
|
-
)}
|
|
1286
|
-
</ComboboxPrimitive.Collection>
|
|
1287
|
-
</ComboboxPrimitive.List>
|
|
1288
|
-
{loading && (
|
|
1289
|
-
// Not a row: no hover, no pick. Under the rows, so items already
|
|
1290
|
-
// here stay where they are while more arrive.
|
|
1291
|
-
<div
|
|
1292
|
-
role="status"
|
|
1293
|
-
className={cn(
|
|
1294
|
-
'flex shrink-0 items-center justify-center text-muted-foreground',
|
|
1295
|
-
sizeClasses.control,
|
|
1296
|
-
sizeClasses.itemPx,
|
|
1297
|
-
sizeClasses.gap,
|
|
1298
|
-
sizeClasses.text,
|
|
1299
|
-
)}
|
|
1300
|
-
>
|
|
1301
|
-
<Loader
|
|
1302
|
-
size={sizeClasses.icon}
|
|
1303
|
-
strokeWidth={1.5}
|
|
1304
|
-
className="shrink-0 animate-spin motion-reduce:animate-none"
|
|
1305
|
-
/>
|
|
1306
|
-
<span className="truncate [text-box:trim-both_cap_alphabetic]">{loadingLabel}</span>
|
|
1307
|
-
</div>
|
|
1308
|
-
)}
|
|
1309
|
-
</ScrollArea>
|
|
1310
|
-
</ComboboxContentContext.Provider>
|
|
1311
|
-
)
|
|
1312
|
-
})
|
|
1313
|
-
|
|
1314
|
-
ComboboxList.displayName = 'ComboboxList'
|
|
1315
|
-
|
|
1316
|
-
// ---------------------------------------------------------------------------
|
|
1317
|
-
// ComboboxItem
|
|
1318
|
-
// ---------------------------------------------------------------------------
|
|
1319
|
-
|
|
1320
|
-
interface ComboboxItemProps extends HTMLAttributes<HTMLDivElement> {
|
|
1321
|
-
/** Leading icon in the row. */
|
|
1322
|
-
icon?: IconComponent
|
|
1323
|
-
/** The item's value — a string item itself, or an object item's `value`. */
|
|
1324
|
-
value: string
|
|
1325
|
-
/** Disables the row. Defaults to `false`. */
|
|
1326
|
-
disabled?: boolean
|
|
1327
|
-
/** The row label. */
|
|
1328
|
-
children?: ReactNode
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
const ComboboxItem = forwardRef<HTMLDivElement, ComboboxItemProps>(
|
|
1332
|
-
({ className, children, icon: Icon, value, disabled = false, ...props }, ref) => {
|
|
1333
|
-
const comboboxCtx = useComboboxContext()
|
|
1334
|
-
const contentCtx = useContext(ComboboxContentContext)
|
|
1335
|
-
const index = useContext(ComboboxItemIndexContext)
|
|
1336
|
-
const internalRef = useRef<HTMLDivElement>(null)
|
|
1337
|
-
const shape = popupShape
|
|
1338
|
-
const sizeClasses = useSize()
|
|
1339
|
-
const compact = sizeClasses.variant === 'compact'
|
|
1340
|
-
const hasMounted = useRef(false)
|
|
1341
|
-
|
|
1342
|
-
useEffect(() => {
|
|
1343
|
-
hasMounted.current = true
|
|
1344
|
-
}, [])
|
|
1345
|
-
|
|
1346
|
-
// Register with fluid hover. Depends on the (stable) registerItem
|
|
1347
|
-
// rather than the content context, which is rebuilt on every activeIndex
|
|
1348
|
-
// change.
|
|
1349
|
-
const registerItem = contentCtx?.registerItem
|
|
1350
|
-
useRegisterFluidHoverItem(registerItem, index, internalRef)
|
|
1351
|
-
|
|
1352
|
-
const isActive = contentCtx?.activeIndex === index
|
|
1353
|
-
const isChecked = comboboxCtx.values.includes(value)
|
|
1354
|
-
const skipAnimation = !hasMounted.current
|
|
1355
|
-
// Base UI matches rows to `items` by value, so the row hands back the
|
|
1356
|
-
// item it was rendered from.
|
|
1357
|
-
const item = comboboxCtx.itemsByValue.get(value) ?? value
|
|
1358
|
-
|
|
1359
|
-
return (
|
|
1360
|
-
<ComboboxPrimitive.Item
|
|
1361
|
-
value={item}
|
|
1362
|
-
index={index}
|
|
1363
|
-
disabled={disabled}
|
|
1364
|
-
render={
|
|
1365
|
-
<div
|
|
1366
|
-
ref={(node: HTMLDivElement | null) => {
|
|
1367
|
-
;(internalRef as MutableRefObject<HTMLDivElement | null>).current = node
|
|
1368
|
-
if (typeof ref === 'function') ref(node)
|
|
1369
|
-
else if (ref) (ref as MutableRefObject<HTMLDivElement | null>).current = node
|
|
1370
|
-
}}
|
|
1371
|
-
data-fluid-hover-index={index}
|
|
1372
|
-
data-value={value}
|
|
1373
|
-
className={cn(
|
|
1374
|
-
// Fixed height so the text-box trim on the label doesn't
|
|
1375
|
-
// shrink the row; shrink-0 because the list is a max-height
|
|
1376
|
-
// flex column.
|
|
1377
|
-
`relative z-10 flex ${sizeClasses.control} shrink-0 items-center ${sizeClasses.gap} ${shape.item} ${sizeClasses.itemPx} ${sizeClasses.text} cursor-pointer outline-none select-none`,
|
|
1378
|
-
'transition-[color] duration-fast',
|
|
1379
|
-
isActive || isChecked ? 'text-foreground' : 'text-muted-foreground',
|
|
1380
|
-
disabled && 'opacity-50 pointer-events-none',
|
|
1381
|
-
className,
|
|
1382
|
-
)}
|
|
1383
|
-
{...props}
|
|
1384
|
-
/>
|
|
1385
|
-
}
|
|
1386
|
-
>
|
|
1387
|
-
{Icon && (
|
|
1388
|
-
<Icon
|
|
1389
|
-
size={sizeClasses.icon}
|
|
1390
|
-
strokeWidth={isActive || isChecked ? 2 : 1.5}
|
|
1391
|
-
className="shrink-0 transition-[color,stroke-width] duration-fast"
|
|
1392
|
-
/>
|
|
1393
|
-
)}
|
|
1394
|
-
|
|
1395
|
-
{/* py-1/-my-1 keeps truncate's overflow:hidden from clipping
|
|
1396
|
-
ascenders/descenders outside the trimmed box. */}
|
|
1397
|
-
{/* Nodes keep their own box; only text is trimmed and truncated. */}
|
|
1398
|
-
<span
|
|
1399
|
-
className={cn(
|
|
1400
|
-
'flex-1 min-w-0',
|
|
1401
|
-
typeof children === 'string' || typeof children === 'number'
|
|
1402
|
-
? 'truncate [text-box:trim-both_cap_alphabetic] py-1 -my-1'
|
|
1403
|
-
: 'flex items-center',
|
|
1404
|
-
)}
|
|
1405
|
-
>
|
|
1406
|
-
{children}
|
|
1407
|
-
</span>
|
|
1408
|
-
|
|
1409
|
-
{/* Always-rendered fixed slot so the check appearing/disappearing
|
|
1410
|
-
never changes the row's intrinsic width. */}
|
|
1411
|
-
<span aria-hidden className={cn('shrink-0', compact ? 'w-3.5 h-3.5' : 'w-4 h-4')}>
|
|
1412
|
-
<AnimatePresence>
|
|
1413
|
-
{isChecked && (
|
|
1414
|
-
<motion.svg
|
|
1415
|
-
key="check"
|
|
1416
|
-
width={sizeClasses.icon}
|
|
1417
|
-
height={sizeClasses.icon}
|
|
1418
|
-
viewBox="0 0 24 24"
|
|
1419
|
-
fill="none"
|
|
1420
|
-
stroke="currentColor"
|
|
1421
|
-
strokeWidth={2}
|
|
1422
|
-
strokeLinecap="round"
|
|
1423
|
-
strokeLinejoin="round"
|
|
1424
|
-
className="text-foreground"
|
|
1425
|
-
initial={{ opacity: 1 }}
|
|
1426
|
-
animate={{ opacity: 1 }}
|
|
1427
|
-
exit={{ opacity: 1 }}
|
|
1428
|
-
>
|
|
1429
|
-
<motion.path
|
|
1430
|
-
d="M4 12L9 17L20 6"
|
|
1431
|
-
initial={{ pathLength: skipAnimation ? 1 : 0 }}
|
|
1432
|
-
animate={{
|
|
1433
|
-
pathLength: 1,
|
|
1434
|
-
transition: { duration: spring.fast.duration, ease: 'easeOut' },
|
|
1435
|
-
}}
|
|
1436
|
-
exit={{
|
|
1437
|
-
pathLength: 0,
|
|
1438
|
-
transition: { duration: 0.04, ease: 'easeIn' },
|
|
1439
|
-
}}
|
|
1440
|
-
/>
|
|
1441
|
-
</motion.svg>
|
|
1442
|
-
)}
|
|
1443
|
-
</AnimatePresence>
|
|
1444
|
-
</span>
|
|
1445
|
-
</ComboboxPrimitive.Item>
|
|
1446
|
-
)
|
|
1447
|
-
},
|
|
1448
|
-
)
|
|
1449
|
-
|
|
1450
|
-
ComboboxItem.displayName = 'ComboboxItem'
|
|
1451
|
-
|
|
1452
|
-
// ---------------------------------------------------------------------------
|
|
1453
|
-
// ComboboxEmpty — shown in place of the list when nothing matches.
|
|
1454
|
-
// ---------------------------------------------------------------------------
|
|
1455
|
-
|
|
1456
|
-
interface ComboboxEmptyProps extends HTMLAttributes<HTMLDivElement> {
|
|
1457
|
-
/** Shown instead of the children when `hideSelected` has emptied the
|
|
1458
|
-
* list with nothing typed: every item is a chip already. */
|
|
1459
|
-
allSelected?: ReactNode
|
|
1460
|
-
/** Shown when nothing matches the query. */
|
|
1461
|
-
children?: ReactNode
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
const ComboboxEmpty = forwardRef<HTMLDivElement, ComboboxEmptyProps>(
|
|
1465
|
-
({ className, children, allSelected, ...props }, ref) => {
|
|
1466
|
-
const sizeClasses = useSize()
|
|
1467
|
-
const ctx = useComboboxContext()
|
|
1468
|
-
return (
|
|
1469
|
-
// Base UI keeps this element mounted (it is a live region) and only
|
|
1470
|
-
// renders the children while the list is empty — so the padding is
|
|
1471
|
-
// gated on having content.
|
|
1472
|
-
<ComboboxPrimitive.Empty
|
|
1473
|
-
ref={ref}
|
|
1474
|
-
className={cn('px-3 text-center text-muted-foreground [&:not(:empty)]:py-6', sizeClasses.text, className)}
|
|
1475
|
-
{...props}
|
|
1476
|
-
>
|
|
1477
|
-
{ctx.loading ? null : ctx.allSelected && allSelected !== undefined ? allSelected : children}
|
|
1478
|
-
</ComboboxPrimitive.Empty>
|
|
1479
|
-
)
|
|
1480
|
-
},
|
|
1481
|
-
)
|
|
1482
|
-
|
|
1483
|
-
ComboboxEmpty.displayName = 'ComboboxEmpty'
|
|
1484
|
-
|
|
1485
|
-
// ---------------------------------------------------------------------------
|
|
1486
|
-
// Statics — the compound API, typed by assignment so `<Combobox.Item>` and
|
|
1487
|
-
// the named exports stay the same component.
|
|
1488
|
-
// ---------------------------------------------------------------------------
|
|
1489
|
-
|
|
1490
|
-
Combobox.Input = ComboboxInput
|
|
1491
|
-
Combobox.Chips = ComboboxChips
|
|
1492
|
-
Combobox.Content = ComboboxContent
|
|
1493
|
-
Combobox.List = ComboboxList
|
|
1494
|
-
Combobox.Item = ComboboxItem
|
|
1495
|
-
Combobox.Empty = ComboboxEmpty
|
|
1496
|
-
|
|
1497
|
-
// ---------------------------------------------------------------------------
|
|
1498
|
-
// Exports
|
|
1499
|
-
// ---------------------------------------------------------------------------
|
|
1500
|
-
|
|
1501
|
-
export type {
|
|
1502
|
-
ComboboxChipsProps,
|
|
1503
|
-
ComboboxContentProps,
|
|
1504
|
-
ComboboxEmptyProps,
|
|
1505
|
-
ComboboxFooter,
|
|
1506
|
-
ComboboxFooterContext,
|
|
1507
|
-
ComboboxInputProps,
|
|
1508
|
-
ComboboxItemData,
|
|
1509
|
-
ComboboxItemObject,
|
|
1510
|
-
ComboboxItemProps,
|
|
1511
|
-
ComboboxLabels,
|
|
1512
|
-
ComboboxListProps,
|
|
1513
|
-
ComboboxProps,
|
|
1514
|
-
ComboboxValue,
|
|
1515
|
-
}
|
|
1516
|
-
export {
|
|
1517
|
-
Combobox,
|
|
1518
|
-
ComboboxChips,
|
|
1519
|
-
ComboboxContent,
|
|
1520
|
-
ComboboxEmpty,
|
|
1521
|
-
ComboboxInput,
|
|
1522
|
-
ComboboxItem,
|
|
1523
|
-
ComboboxList,
|
|
1524
|
-
fieldVariants as comboboxFieldVariants,
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
export default Combobox
|