@justanarthur/modo-ui 0.5.0 → 0.6.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +608 -0
- package/README.md +61 -39
- package/blocks/sidebar-app/examples.mdx +33 -0
- package/blocks/sidebar-app/index.tsx +218 -0
- package/blocks/sidebar-app/inset-topbar.tsx +25 -0
- package/blocks/sidebar-app/menu-grid.ts +26 -0
- package/blocks/sidebar-app/nav-data.ts +36 -0
- package/blocks/sidebar-app/search-field.tsx +48 -0
- package/blocks/sidebar-app/user-footer.tsx +67 -0
- package/blocks/sidebar-app/workspace-header.tsx +129 -0
- package/components/AI/input-message/examples.mdx +124 -0
- package/components/AI/input-message/file-thumbnail.tsx +173 -0
- package/components/AI/input-message/index.tsx +1383 -0
- package/components/AI/thinking-indicator/examples.mdx +27 -0
- package/components/AI/thinking-indicator/index.tsx +210 -0
- package/components/accordion/examples.mdx +148 -0
- package/components/accordion/index.tsx +1034 -0
- package/components/avatar/examples.mdx +95 -0
- package/components/avatar/index.tsx +129 -0
- package/components/badge/change-demo.tsx +29 -0
- package/components/badge/examples.mdx +120 -0
- package/components/badge/index.tsx +225 -0
- package/components/badge/variants.ts +73 -0
- package/components/button/examples.mdx +164 -0
- package/components/button/index.tsx +416 -0
- package/components/button/loading-demo.tsx +25 -0
- package/components/button/variants.ts +166 -0
- package/components/card/examples.mdx +212 -0
- package/components/card/index.tsx +1037 -0
- package/components/copy/examples.mdx +87 -0
- package/components/copy/index.tsx +482 -0
- package/components/inputs/checkbox/examples.mdx +83 -0
- package/components/inputs/checkbox/index.tsx +611 -0
- package/components/inputs/checkbox/select-all-demo.tsx +48 -0
- package/components/inputs/color-picker/examples.mdx +72 -0
- package/components/inputs/color-picker/index.tsx +1955 -0
- package/components/inputs/color-picker/variants.ts +401 -0
- package/components/inputs/combobox/examples.mdx +273 -0
- package/components/inputs/combobox/index.tsx +1436 -0
- package/components/inputs/combobox/remote-demo.tsx +101 -0
- package/components/inputs/date-time/calendar/examples.mdx +34 -0
- package/components/inputs/date-time/calendar/index.tsx +351 -0
- package/components/inputs/date-time/calendar/use-month-window.ts +163 -0
- package/components/inputs/date-time/date-picker/examples.mdx +166 -0
- package/components/inputs/date-time/date-picker/index.tsx +385 -0
- package/components/inputs/date-time/date-picker/masked-date-input.tsx +336 -0
- package/components/inputs/date-time/date-range-picker/examples.mdx +29 -0
- package/components/inputs/date-time/date-range-picker/index.tsx +217 -0
- package/components/inputs/date-time/duration-picker/examples.mdx +16 -0
- package/components/inputs/date-time/duration-picker/index.tsx +189 -0
- package/components/inputs/date-time/month-picker/examples.mdx +16 -0
- package/components/inputs/date-time/month-picker/index.tsx +190 -0
- package/components/inputs/date-time/picker-shell.tsx +395 -0
- package/components/inputs/date-time/time-columns.tsx +91 -0
- package/components/inputs/date-time/time-picker/examples.mdx +26 -0
- package/components/inputs/date-time/time-picker/index.tsx +166 -0
- package/components/inputs/field/examples.mdx +113 -0
- package/components/inputs/field/index.tsx +267 -0
- package/components/inputs/file-dropzone/examples.mdx +73 -0
- package/components/inputs/file-dropzone/file-row.tsx +201 -0
- package/components/inputs/file-dropzone/index.tsx +296 -0
- package/components/inputs/file-dropzone/upload-demo.tsx +67 -0
- package/components/inputs/file-dropzone/variants.ts +94 -0
- package/components/inputs/input/examples.mdx +135 -0
- package/components/inputs/input/index.tsx +494 -0
- package/components/inputs/input/password-demo.tsx +30 -0
- package/components/inputs/label/examples.mdx +67 -0
- package/components/inputs/label/index.tsx +85 -0
- package/components/inputs/number-input/examples.mdx +56 -0
- package/components/inputs/number-input/index.tsx +214 -0
- package/components/inputs/radio/examples.mdx +77 -0
- package/components/inputs/radio/index.tsx +636 -0
- package/components/inputs/search/examples.mdx +74 -0
- package/components/inputs/search/index.tsx +333 -0
- package/components/inputs/select/examples.mdx +218 -0
- package/components/inputs/select/index.tsx +862 -0
- package/components/inputs/select/variants.ts +32 -0
- package/components/inputs/slider/examples.mdx +127 -0
- package/components/inputs/slider/index.tsx +1725 -0
- package/components/inputs/switch/examples.mdx +94 -0
- package/components/inputs/switch/index.tsx +504 -0
- package/components/inputs/textarea/examples.mdx +88 -0
- package/components/inputs/textarea/index.tsx +161 -0
- package/components/navigation/navigation-menu/examples.mdx +109 -0
- package/components/navigation/navigation-menu/index.tsx +613 -0
- package/components/navigation/pagination/examples.mdx +64 -0
- package/components/navigation/pagination/index.tsx +395 -0
- package/components/navigation/sidebar/examples.mdx +319 -0
- package/components/navigation/sidebar/index.tsx +453 -0
- package/components/navigation/sidebar/sidebar-core.tsx +1408 -0
- package/components/navigation/sidebar/sidebar-menu.tsx +1495 -0
- package/components/navigation/sidebar/sidebar.css +74 -0
- package/components/navigation/sidebar/variants.ts +55 -0
- package/components/navigation/tabs/examples.mdx +89 -0
- package/components/navigation/tabs/index.tsx +494 -0
- package/components/navigation/tabs-subtle/examples.mdx +49 -0
- package/components/navigation/tabs-subtle/index.tsx +512 -0
- package/components/overlays/alert-dialog/examples.mdx +123 -0
- package/components/overlays/alert-dialog/index.tsx +258 -0
- package/components/overlays/alert-dialog/pending-demo.tsx +36 -0
- package/components/overlays/command/examples.mdx +113 -0
- package/components/overlays/command/index.tsx +416 -0
- package/components/overlays/context-menu/examples.mdx +56 -0
- package/components/overlays/context-menu/index.tsx +279 -0
- package/components/overlays/dialog/examples.mdx +144 -0
- package/components/overlays/dialog/index.tsx +366 -0
- package/components/overlays/dropdown/dropdown-search.tsx +395 -0
- package/components/overlays/dropdown/examples.mdx +294 -0
- package/components/overlays/dropdown/index.tsx +1168 -0
- package/components/overlays/dropdown/menu-item.tsx +464 -0
- package/components/overlays/dropdown/menu-parts.tsx +660 -0
- package/components/overlays/popover/anchored-demo.tsx +59 -0
- package/components/overlays/popover/examples.mdx +180 -0
- package/components/overlays/popover/index.tsx +315 -0
- package/components/overlays/preview-card/examples.mdx +61 -0
- package/components/overlays/preview-card/index.tsx +204 -0
- package/components/overlays/sheet/examples.mdx +89 -0
- package/components/overlays/sheet/index.tsx +247 -0
- package/components/overlays/toast/examples.mdx +68 -0
- package/components/overlays/toast/global-demo.tsx +72 -0
- package/components/overlays/toast/index.tsx +474 -0
- package/components/overlays/toast/manager.ts +193 -0
- package/components/overlays/tooltip/examples.mdx +106 -0
- package/components/overlays/tooltip/index.tsx +519 -0
- package/components/progress/examples.mdx +59 -0
- package/components/progress/index.tsx +174 -0
- package/components/progress/progress.css +49 -0
- package/components/progress/step-demo.tsx +27 -0
- package/components/table/examples.mdx +184 -0
- package/components/table/index.tsx +510 -0
- package/components/table/sort-demo.tsx +61 -0
- package/dist/base.css +100 -0
- package/dist/blocks/sidebar-app/index.js +39 -26
- package/dist/components/AI/input-message/index.js +22 -16
- package/dist/components/AI/thinking-indicator/index.js +9 -6
- package/dist/components/accordion/index.js +16 -13
- package/dist/components/avatar/index.d.ts +46 -0
- package/dist/components/avatar/index.js +18 -0
- package/dist/components/badge/index.d.ts +18 -11
- package/dist/components/badge/index.js +11 -7
- package/dist/components/badge/variants.d.ts +33 -0
- package/dist/components/badge/variants.js +10 -0
- package/dist/components/button/index.d.ts +53 -17
- package/dist/components/button/index.js +10 -6
- package/dist/components/button/variants.d.ts +39 -0
- package/dist/components/button/variants.js +8 -0
- package/dist/components/card/index.d.ts +16 -3
- package/dist/components/card/index.js +16 -11
- package/dist/components/copy/index.js +13 -9
- package/dist/components/inputs/checkbox/index.d.ts +28 -9
- package/dist/components/inputs/checkbox/index.js +17 -13
- package/dist/components/inputs/color-picker/index.d.ts +13 -13
- package/dist/components/inputs/color-picker/index.js +25 -18
- package/dist/components/inputs/color-picker/variants.d.ts +61 -0
- package/dist/components/inputs/color-picker/variants.js +28 -0
- package/dist/components/inputs/combobox/index.d.ts +60 -17
- package/dist/components/inputs/combobox/index.js +26 -18
- package/dist/components/inputs/date-time/calendar/index.js +14 -11
- package/dist/components/inputs/date-time/date-picker/index.d.ts +53 -11
- package/dist/components/inputs/date-time/date-picker/index.js +33 -25
- package/dist/components/inputs/date-time/date-range-picker/index.d.ts +3 -3
- package/dist/components/inputs/date-time/date-range-picker/index.js +32 -24
- package/dist/components/inputs/date-time/duration-picker/index.d.ts +3 -3
- package/dist/components/inputs/date-time/duration-picker/index.js +32 -24
- package/dist/components/inputs/date-time/month-picker/index.d.ts +3 -3
- package/dist/components/inputs/date-time/month-picker/index.js +31 -23
- package/dist/components/inputs/date-time/time-picker/index.d.ts +3 -3
- package/dist/components/inputs/date-time/time-picker/index.js +32 -24
- package/dist/components/inputs/field/index.d.ts +116 -0
- package/dist/components/inputs/field/index.js +28 -0
- package/dist/components/inputs/file-dropzone/index.d.ts +165 -0
- package/dist/components/inputs/file-dropzone/index.js +32 -0
- package/dist/components/inputs/file-dropzone/variants.d.ts +35 -0
- package/dist/components/inputs/file-dropzone/variants.js +12 -0
- package/dist/components/inputs/input/index.d.ts +95 -13
- package/dist/components/inputs/input/index.js +17 -9
- package/dist/components/inputs/label/index.d.ts +32 -0
- package/dist/components/inputs/label/index.js +11 -0
- package/dist/components/inputs/number-input/index.d.ts +84 -0
- package/dist/components/inputs/number-input/index.js +20 -0
- package/dist/components/inputs/radio/index.d.ts +20 -5
- package/dist/components/inputs/radio/index.js +15 -12
- package/dist/components/inputs/search/index.js +25 -18
- package/dist/components/inputs/select/index.d.ts +41 -11
- package/dist/components/inputs/select/index.js +27 -14
- package/dist/components/inputs/select/variants.d.ts +5 -0
- package/dist/components/inputs/select/variants.js +6 -0
- package/dist/components/inputs/slider/index.js +7 -5
- package/dist/components/inputs/switch/index.d.ts +8 -2
- package/dist/components/inputs/switch/index.js +14 -12
- package/dist/components/inputs/textarea/index.d.ts +4 -0
- package/dist/components/inputs/textarea/index.js +8 -6
- package/dist/components/navigation/navigation-menu/index.d.ts +114 -0
- package/dist/components/navigation/navigation-menu/index.js +40 -0
- package/dist/components/navigation/pagination/index.d.ts +73 -0
- package/dist/components/navigation/pagination/index.js +39 -0
- package/dist/components/navigation/sidebar/index.d.ts +44 -21
- package/dist/components/navigation/sidebar/index.js +41 -31
- package/dist/components/navigation/sidebar/variants.d.ts +25 -0
- package/dist/components/navigation/sidebar/variants.js +28 -0
- package/dist/components/navigation/tabs/index.d.ts +8 -2
- package/dist/components/navigation/tabs/index.js +14 -10
- package/dist/components/navigation/tabs-subtle/index.js +12 -10
- package/dist/components/overlays/alert-dialog/index.d.ts +121 -15
- package/dist/components/overlays/alert-dialog/index.js +22 -13
- package/dist/components/overlays/command/index.js +24 -17
- package/dist/components/overlays/context-menu/index.d.ts +33 -17
- package/dist/components/overlays/context-menu/index.js +27 -18
- package/dist/components/overlays/dialog/index.js +17 -12
- package/dist/components/overlays/dropdown/index.d.ts +257 -31
- package/dist/components/overlays/dropdown/index.js +42 -17
- package/dist/components/overlays/popover/index.d.ts +31 -9
- package/dist/components/overlays/popover/index.js +12 -9
- package/dist/components/overlays/preview-card/index.js +11 -8
- package/dist/components/overlays/sheet/index.js +18 -13
- package/dist/components/overlays/toast/index.d.ts +123 -47
- package/dist/components/overlays/toast/index.js +18 -10
- package/dist/components/overlays/tooltip/index.d.ts +113 -21
- package/dist/components/overlays/tooltip/index.js +15 -6
- package/dist/components/progress/index.d.ts +56 -0
- package/dist/components/progress/index.js +15 -0
- package/dist/components/table/index.d.ts +63 -8
- package/dist/components/table/index.js +19 -9
- package/dist/index.d.ts +2201 -736
- package/dist/index.js +223 -82
- package/dist/lib/date-locale.d.ts +14 -0
- package/dist/lib/date-locale.js +16 -0
- package/dist/lib/field-classes.d.ts +144 -0
- package/dist/lib/field-classes.js +40 -0
- package/dist/lib/field-variants.d.ts +47 -0
- package/dist/lib/field-variants.js +26 -0
- package/dist/lib/fluid-hover-highlight.d.ts +112 -0
- package/dist/lib/fluid-hover-highlight.js +17 -0
- package/dist/lib/focus-ring.d.ts +18 -0
- package/dist/lib/focus-ring.js +10 -0
- package/dist/lib/goo-indicator.d.ts +46 -0
- package/dist/lib/goo-indicator.js +14 -0
- package/dist/lib/icon-context.d.ts +30 -0
- package/dist/lib/icon-context.js +12 -0
- package/dist/lib/inert.d.ts +9 -0
- package/dist/lib/inert.js +8 -0
- package/dist/lib/morph-layers.d.ts +93 -0
- package/dist/lib/morph-layers.js +13 -0
- package/dist/lib/morph-part.d.ts +21 -0
- package/dist/lib/morph-part.js +12 -0
- package/dist/lib/popup.d.ts +20 -0
- package/dist/lib/popup.js +12 -0
- package/dist/lib/reduced-motion.d.ts +9 -0
- package/dist/lib/reduced-motion.js +6 -0
- package/dist/lib/row-index.d.ts +10 -0
- package/dist/lib/row-index.js +8 -0
- package/dist/lib/row-order.d.ts +28 -0
- package/dist/lib/row-order.js +10 -0
- package/dist/lib/scroll-column.d.ts +27 -0
- package/dist/lib/scroll-column.js +18 -0
- package/dist/lib/shape-context.d.ts +30 -0
- package/dist/lib/shape-context.js +12 -0
- package/dist/lib/size-context.d.ts +94 -0
- package/dist/lib/size-context.js +20 -0
- package/dist/lib/size-scale.d.ts +79 -0
- package/dist/lib/size-scale.js +8 -0
- package/dist/lib/slot.d.ts +12 -0
- package/dist/lib/slot.js +6 -0
- package/dist/lib/springs.d.ts +44 -0
- package/dist/lib/springs.js +8 -0
- package/dist/lib/surface-classes.d.ts +19 -0
- package/dist/lib/surface-classes.js +10 -0
- package/dist/lib/surface-context.d.ts +9 -0
- package/dist/lib/surface-context.js +8 -0
- package/dist/lib/use-controllable-state.d.ts +2 -0
- package/dist/lib/use-controllable-state.js +6 -0
- package/dist/lib/use-fluid-hover.d.ts +138 -0
- package/dist/lib/use-fluid-hover.js +14 -0
- package/dist/lib/use-is-mobile.d.ts +2 -0
- package/dist/lib/use-is-mobile.js +6 -0
- package/dist/lib/use-keyboard-nav-gate.d.ts +20 -0
- package/dist/lib/use-keyboard-nav-gate.js +7 -0
- package/dist/lib/use-merge-split.d.ts +47 -0
- package/dist/lib/use-merge-split.js +15 -0
- package/dist/lib/use-morph.d.ts +101 -0
- package/dist/lib/use-morph.js +20 -0
- package/dist/lib/use-touch-primary.d.ts +2 -0
- package/dist/lib/use-touch-primary.js +6 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.js +6 -0
- package/dist/preset.d.ts +17 -0
- package/dist/preset.js +22 -2
- package/dist/primitives/code/index.js +7 -5
- package/dist/primitives/craft/index.js +9 -6
- package/dist/primitives/fluid-hover/index.js +9 -8
- package/dist/primitives/morph/index.js +15 -10
- package/dist/primitives/motion/index.js +10 -8
- package/dist/primitives/scroll-area/index.js +4 -3
- package/dist/primitives/separator/index.d.ts +25 -0
- package/dist/primitives/separator/index.js +9 -0
- package/dist/primitives/sizes/index.js +6 -4
- package/dist/primitives/skeleton/index.d.ts +35 -0
- package/dist/primitives/skeleton/index.js +12 -0
- package/dist/primitives/surface/index.js +6 -4
- package/dist/shared/{components-3mkepka5.js → components-0jmqhx2k.js} +28 -25
- package/dist/shared/components-124epb7f.js +53 -0
- package/dist/shared/{components-gz8zf3ec.js → components-1k0wv53d.js} +15 -43
- package/dist/shared/components-26snnbs9.js +322 -0
- package/dist/shared/{components-2dxkv6zm.js → components-2c0qcfx4.js} +8 -7
- package/dist/shared/{components-473g814w.js → components-2mt9te6d.js} +243 -137
- package/dist/shared/{components-hawvy2qz.js → components-2z48g4fp.js} +9 -30
- package/dist/shared/components-34txk9rj.js +35 -0
- package/dist/shared/components-3h92d94m.js +23 -0
- package/dist/shared/{components-zfnmn5d4.js → components-3sp623ae.js} +36 -31
- package/dist/shared/{components-jwst3vtj.js → components-3wxkjgqn.js} +186 -76
- package/dist/shared/{components-s2bz1xpk.js → components-49bsynbw.js} +8 -5
- package/dist/shared/components-4bwsd46w.js +71 -0
- package/dist/shared/components-4n4f5m3e.js +297 -0
- package/dist/shared/{components-0q0sqfh4.js → components-4y816hwp.js} +27 -49
- package/dist/shared/components-52ngsgca.js +399 -0
- package/dist/shared/{components-y9y0g8er.js → components-52r5763g.js} +22 -19
- package/dist/shared/components-559tf7yb.js +28 -0
- package/dist/shared/components-5mkga9hj.js +440 -0
- package/dist/shared/{components-kwa6mrf9.js → components-5s0nfqvw.js} +14 -9
- package/dist/shared/{components-dk0gjr5a.js → components-60atskhp.js} +19 -18
- package/dist/shared/{components-rr7a6ja9.js → components-658b26nb.js} +21 -18
- package/dist/shared/{components-xxvdw7k1.js → components-65vwfjs6.js} +37 -34
- package/dist/shared/{components-frw34b65.js → components-6j2fh74k.js} +114 -123
- package/dist/shared/components-6zg1yft7.js +167 -0
- package/dist/shared/components-6zng858q.js +53 -0
- package/dist/shared/components-72w4ge68.js +64 -0
- package/dist/shared/{components-cv16mryv.js → components-78x40gee.js} +13 -7
- package/dist/shared/{components-b2wyq2cq.js → components-7d4r7hh1.js} +39 -57
- package/dist/shared/components-7ntvvegr.js +316 -0
- package/dist/shared/components-8nwsqjn8.js +34 -0
- package/dist/shared/{components-5xc2j8jz.js → components-8tt3xbb5.js} +19 -18
- package/dist/shared/{components-vc7vq5nz.js → components-9as3rf68.js} +6 -3
- package/dist/shared/components-9mys9jna.js +255 -0
- package/dist/shared/components-adkarj8n.js +35 -0
- package/dist/shared/{components-b3c5et5b.js → components-ae9fncz4.js} +32 -31
- package/dist/shared/{components-hqeq3w9q.js → components-awra5sff.js} +24 -16
- package/dist/shared/components-b1e239g3.js +28 -0
- package/dist/shared/{components-q5xj5c16.js → components-b1f2bjf1.js} +9 -8
- package/dist/shared/{components-qmmjx1qw.js → components-bn42vq6d.js} +71 -72
- package/dist/shared/components-c96g82th.js +1444 -0
- package/dist/shared/components-c9yr4ed1.js +44 -0
- package/dist/shared/components-d3shbwqq.js +95 -0
- package/dist/shared/{components-jjw69c4c.js → components-dcc02hs2.js} +7 -4
- package/dist/shared/{components-men63rwj.js → components-de99ykt7.js} +3 -2
- package/dist/shared/components-dhgk6mbt.js +16 -0
- package/dist/shared/components-drxrtnp2.js +25 -0
- package/dist/shared/{components-6p8tfcd6.js → components-ds9fqat9.js} +21 -18
- package/dist/shared/{components-mjthm1kf.js → components-egy4tyjt.js} +11 -8
- package/dist/shared/components-f0xfpkhh.js +82 -0
- package/dist/shared/components-f7h57m6t.js +59 -0
- package/dist/shared/{components-d6xtjht3.js → components-fjkpbg6n.js} +33 -30
- package/dist/shared/{components-gc5vtq2f.js → components-gh0qeey3.js} +63 -29
- package/dist/shared/{components-39jq9va1.js → components-gvbapfen.js} +29 -24
- package/dist/shared/{components-5vgpnxgy.js → components-gzesg7hy.js} +19 -16
- package/dist/shared/components-h6p86be3.js +19 -0
- package/dist/shared/components-h83az0bz.js +33 -0
- package/dist/shared/components-jjrj6ymn.js +104 -0
- package/dist/shared/{components-wvp1m52g.js → components-jp5v0m0f.js} +15 -14
- package/dist/shared/{components-zrkzcayz.js → components-jqpefjsn.js} +54 -6
- package/dist/shared/{components-1z6tdadk.js → components-k74j4hwh.js} +20 -17
- package/dist/shared/components-kswxnm3h.js +24 -0
- package/dist/shared/{components-jbbmdr74.js → components-mkkbbgb7.js} +36 -17
- package/dist/shared/{components-pbmqctfa.js → components-mpw667rb.js} +169 -65
- package/dist/shared/{components-s5ngg32c.js → components-paa08427.js} +121 -47
- package/dist/shared/{components-vfbhxf5r.js → components-pd42emw3.js} +6 -5
- package/dist/shared/{components-hw9yk32z.js → components-pmfszv7a.js} +35 -23
- package/dist/shared/{components-chacnk61.js → components-pw06rbxq.js} +19 -19
- package/dist/shared/components-q03cktx8.js +246 -0
- package/dist/shared/{components-kvr7f9hy.js → components-qdm3yh26.js} +19 -10
- package/dist/shared/{components-27nnvrwe.js → components-qs4rkqsj.js} +15 -14
- package/dist/shared/{components-s083f38z.js → components-r6gbmkew.js} +22 -21
- package/dist/shared/{components-ta7shw28.js → components-r8eqs7gz.js} +4 -4
- package/dist/shared/{components-sn1rxx1d.js → components-rm02h9nt.js} +12 -11
- package/dist/shared/{components-c6s6b43g.js → components-rrbpmfx4.js} +3 -2
- package/dist/shared/{components-n7mjr357.js → components-tz1fv35x.js} +3 -2
- package/dist/shared/components-tz4gaf1v.js +23 -0
- package/dist/shared/{components-zx6b6yby.js → components-vf45j12c.js} +4 -18
- package/dist/shared/{components-3ky44908.js → components-vfjc9jzz.js} +2 -3
- package/dist/shared/{components-39vmfq9s.js → components-vsa625vk.js} +21 -20
- package/dist/shared/components-wawnmkph.js +500 -0
- package/dist/shared/components-wktrrb4m.js +38 -0
- package/dist/shared/{components-8snj490h.js → components-wwd363me.js} +78 -32
- package/dist/shared/{components-5pnw8183.js → components-xcwvgcrd.js} +23 -22
- package/dist/shared/components-xdqmt6ed.js +20 -0
- package/dist/shared/{components-h2ajmhwn.js → components-y1w3a6wx.js} +107 -35
- package/dist/shared/{components-ar66gnt0.js → components-y6578b8h.js} +37 -34
- package/dist/shared/{components-59efxjd9.js → components-yhgq0qxx.js} +13 -12
- package/dist/shared/{components-jthe1pwb.js → components-yz84gpkq.js} +101 -404
- package/dist/shared/components-z3v0n0de.js +11 -0
- package/dist/shared/{components-7r3etddy.js → components-z9n19fyk.js} +19 -54
- package/dist/shared/components-zbvjzh1p.js +141 -0
- package/dist/styles.css +241 -16
- package/dist/tailwind.css +13 -0
- package/dist/tokens.css +34 -0
- package/index.ts +60 -0
- package/lib/date-locale.ts +50 -0
- package/lib/field-classes.ts +115 -0
- package/lib/field-variants.ts +117 -0
- package/lib/fluid-hover-highlight.tsx +139 -0
- package/lib/focus-ring.tsx +46 -0
- package/lib/goo-indicator.tsx +261 -0
- package/lib/icon-context.tsx +310 -0
- package/lib/inert.ts +22 -0
- package/lib/morph-layers.tsx +102 -0
- package/lib/morph-part.tsx +49 -0
- package/lib/popup.ts +52 -0
- package/lib/reduced-motion.ts +19 -0
- package/lib/row-index.tsx +29 -0
- package/lib/row-order.tsx +115 -0
- package/lib/scroll-column.tsx +244 -0
- package/lib/shape-context.tsx +124 -0
- package/lib/size-context.tsx +66 -0
- package/lib/size-scale.ts +115 -0
- package/lib/slot.ts +25 -0
- package/lib/springs.ts +62 -0
- package/lib/surface-classes.ts +58 -0
- package/lib/surface-context.tsx +21 -0
- package/lib/use-controllable-state.ts +44 -0
- package/lib/use-fluid-hover.ts +541 -0
- package/lib/use-is-mobile.ts +25 -0
- package/lib/use-keyboard-nav-gate.ts +45 -0
- package/lib/use-merge-split.tsx +209 -0
- package/lib/use-morph.ts +557 -0
- package/lib/use-touch-primary.tsx +43 -0
- package/lib/utils.ts +37 -0
- package/package.json +177 -4
- package/preset.ts +196 -0
- package/primitives/code/examples.mdx +27 -0
- package/primitives/code/index.tsx +80 -0
- package/primitives/craft/demos.tsx +173 -0
- package/primitives/craft/examples.mdx +138 -0
- package/primitives/craft/index.tsx +76 -0
- package/primitives/fluid-hover/examples.mdx +84 -0
- package/primitives/fluid-hover/index.tsx +316 -0
- package/primitives/morph/examples.mdx +135 -0
- package/primitives/morph/index.tsx +192 -0
- package/primitives/motion/examples.mdx +100 -0
- package/primitives/motion/index.tsx +160 -0
- package/primitives/scroll-area/examples.mdx +58 -0
- package/primitives/scroll-area/index.tsx +200 -0
- package/primitives/separator/examples.mdx +30 -0
- package/primitives/separator/index.tsx +53 -0
- package/primitives/sizes/examples.mdx +167 -0
- package/primitives/sizes/index.tsx +89 -0
- package/primitives/skeleton/examples.mdx +51 -0
- package/primitives/skeleton/index.tsx +72 -0
- package/primitives/skeleton/skeleton.css +15 -0
- package/primitives/surface/examples.mdx +173 -0
- package/primitives/surface/index.tsx +109 -0
- package/primitives/surface/surface.css +176 -0
- package/dist/shared/components-0sn7ypjw.js +0 -61
- package/dist/shared/components-1434p1mh.js +0 -123
- package/dist/shared/components-a86dytt0.js +0 -986
- package/dist/shared/components-gf5s6ap9.js +0 -42
- package/dist/shared/components-hyq23phv.js +0 -310
- package/dist/shared/components-ng72k97e.js +0 -128
- package/dist/shared/components-raknhppb.js +0 -64
- package/dist/shared/components-vynjtrk6.js +0 -19
- package/dist/shared/components-wy9jpqny.js +0 -146
- /package/dist/shared/{components-1v7rxytx.js → components-3ey0hzpb.js} +0 -0
- /package/dist/shared/{components-ezxhrmer.js → components-jcnjay5g.js} +0 -0
|
@@ -0,0 +1,395 @@
|
|
|
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/default/dropdown-search.tsx` @ b3587bdbd83fc66c2a6aae3817ffb856cb09260b
|
|
5
|
+
* MIT License © 2026 Micka Touillaud — fluidfunctionalism.com (notice: LICENSE.fluid-functionalism)
|
|
6
|
+
* Local modifications:
|
|
7
|
+
* - `"use client"` dropped; `@/lib/*` imports rewritten to `../../../lib/*`.
|
|
8
|
+
* - `filter` mode (local addition): `value` / `onValueChange` become optional
|
|
9
|
+
* and, with `filter`, the field reads and writes the query the surrounding
|
|
10
|
+
* panel holds (`DropdownFilterContext`), which filters the rows itself. The
|
|
11
|
+
* controlled `value` + `onValueChange` API is untouched.
|
|
12
|
+
* - Styling reads DS tokens (AGENTS.md styling): `duration-80|120|160` and
|
|
13
|
+
* tier-length JS durations → `duration-<tier>` / `spring.*`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
createContext,
|
|
18
|
+
forwardRef,
|
|
19
|
+
type HTMLAttributes,
|
|
20
|
+
type InputHTMLAttributes,
|
|
21
|
+
type KeyboardEvent as ReactKeyboardEvent,
|
|
22
|
+
type RefObject,
|
|
23
|
+
useCallback,
|
|
24
|
+
useContext,
|
|
25
|
+
useEffect,
|
|
26
|
+
useMemo,
|
|
27
|
+
useRef,
|
|
28
|
+
useState,
|
|
29
|
+
} from 'react'
|
|
30
|
+
import { useIcon } from '../../../lib/icon-context'
|
|
31
|
+
import { useSize } from '../../../lib/size-context'
|
|
32
|
+
import { SURFACE_BG } from '../../../lib/surface-classes'
|
|
33
|
+
import { useSurface } from '../../../lib/surface-context'
|
|
34
|
+
import { cn } from '../../../lib/utils'
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Search inside a dropdown menu.
|
|
38
|
+
//
|
|
39
|
+
// DropdownSearch is a text field pinned to the top of a DropdownContent popup.
|
|
40
|
+
// It is controlled: the consumer filters the MenuItems it renders against
|
|
41
|
+
// `value`, so the list can be re-indexed freely (fluid hover keys on the
|
|
42
|
+
// indices of whatever is currently mounted). Two things make a field inside a
|
|
43
|
+
// menu behave:
|
|
44
|
+
//
|
|
45
|
+
// 1. The menu primitive's typeahead must not steal keystrokes from the
|
|
46
|
+
// field — every typing key stops propagating at the input.
|
|
47
|
+
// 2. Typing while a ROW is focused must land in the field: the popup
|
|
48
|
+
// hosts a capture-phase keydown handler (useDropdownSearchHost) that
|
|
49
|
+
// refocuses the input and appends the character.
|
|
50
|
+
//
|
|
51
|
+
// Arrow keys leave the field for the list (first / last row), Enter picks
|
|
52
|
+
// the first row, Escape closes the menu as usual. While the field has focus
|
|
53
|
+
// the first row carries the hover background, so what Enter will pick is
|
|
54
|
+
// always in view; it follows the query as the rows re-filter.
|
|
55
|
+
//
|
|
56
|
+
// Local addition — `filter`: the same field, with the query held by the
|
|
57
|
+
// surrounding panel instead of the consumer. The panel then drops the rows
|
|
58
|
+
// whose `label` doesn't match and re-indexes the survivors, which is exactly
|
|
59
|
+
// the work the controlled form asks the consumer to do.
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
interface SearchHandle {
|
|
63
|
+
input: HTMLInputElement | null
|
|
64
|
+
append: (text: string) => void
|
|
65
|
+
deleteBackward: () => void
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface DropdownSearchHostValue {
|
|
69
|
+
register: (handle: SearchHandle) => () => void
|
|
70
|
+
/** Whether the popup is open. Popups stay mounted through their exit
|
|
71
|
+
* animation, so the field watches this rather than its own mount. */
|
|
72
|
+
open: boolean
|
|
73
|
+
/** Move the hover background to the first enabled row (what Enter picks). */
|
|
74
|
+
highlightFirst: () => void
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const DropdownSearchHostContext = createContext<DropdownSearchHostValue | null>(null)
|
|
78
|
+
|
|
79
|
+
/** Local addition. The query a filtering panel holds for its `filter` field. */
|
|
80
|
+
export interface DropdownFilterContextValue {
|
|
81
|
+
query: string
|
|
82
|
+
setQuery: (query: string) => void
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const DropdownFilterContext = createContext<DropdownFilterContextValue | null>(null)
|
|
86
|
+
|
|
87
|
+
const ROW_SELECTOR = [
|
|
88
|
+
'[role="menuitem"]:not([aria-disabled="true"])',
|
|
89
|
+
'[role="menuitemradio"]:not([aria-disabled="true"])',
|
|
90
|
+
'[role="menuitemcheckbox"]:not([aria-disabled="true"])',
|
|
91
|
+
].join(', ')
|
|
92
|
+
|
|
93
|
+
function menuRows(from: HTMLElement | null): HTMLElement[] {
|
|
94
|
+
const menu = from?.closest<HTMLElement>('[role="menu"]')
|
|
95
|
+
return menu ? Array.from(menu.querySelectorAll<HTMLElement>(ROW_SELECTOR)) : []
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface DropdownSearchHostOptions {
|
|
99
|
+
/** The rows' container: where the first enabled row is looked up. */
|
|
100
|
+
containerRef: RefObject<HTMLElement | null>
|
|
101
|
+
/** The popup's fluid-hover setter, for the first-row highlight. */
|
|
102
|
+
setActiveIndex: (index: number | null) => void
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Hosts a DropdownSearch inside a popup. Returns the context value to
|
|
107
|
+
* provide, a capture-phase keydown handler for the popup element,
|
|
108
|
+
* `hasSearch()` / `searchMounted` for decisions that depend on a field
|
|
109
|
+
* being present, and the first-row highlight the field keeps while it has
|
|
110
|
+
* focus (`isSearchField` / `highlightFirst`, for the popup's focus and
|
|
111
|
+
* mouse-leave handlers).
|
|
112
|
+
*/
|
|
113
|
+
export function useDropdownSearchHost(open: boolean, { containerRef, setActiveIndex }: DropdownSearchHostOptions) {
|
|
114
|
+
const handleRef = useRef<SearchHandle | null>(null)
|
|
115
|
+
// Reactive twin of the ref, for render-time decisions (the popup drops its
|
|
116
|
+
// scroll fade while a field is pinned at the top).
|
|
117
|
+
const [searchMounted, setSearchMounted] = useState(false)
|
|
118
|
+
|
|
119
|
+
const register = useCallback((handle: SearchHandle) => {
|
|
120
|
+
handleRef.current = handle
|
|
121
|
+
setSearchMounted(true)
|
|
122
|
+
return () => {
|
|
123
|
+
if (handleRef.current === handle) {
|
|
124
|
+
handleRef.current = null
|
|
125
|
+
setSearchMounted(false)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}, [])
|
|
129
|
+
|
|
130
|
+
// The row Enter will pick carries the hover background while the field
|
|
131
|
+
// has focus. Rows re-index from 0 on every filter, so this is looked up
|
|
132
|
+
// in the DOM each time rather than remembered.
|
|
133
|
+
const highlightFirst = useCallback(() => {
|
|
134
|
+
const first = containerRef.current?.querySelector<HTMLElement>(ROW_SELECTOR)
|
|
135
|
+
const index = first?.getAttribute('data-fluid-hover-index')
|
|
136
|
+
setActiveIndex(index != null ? Number(index) : null)
|
|
137
|
+
}, [containerRef, setActiveIndex])
|
|
138
|
+
|
|
139
|
+
/** Whether `target` is the mounted search field. */
|
|
140
|
+
const isSearchField = useCallback(
|
|
141
|
+
(target: EventTarget | null) => target !== null && target === handleRef.current?.input,
|
|
142
|
+
[],
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
const host = useMemo(() => ({ register, open, highlightFirst }), [register, open, highlightFirst])
|
|
146
|
+
|
|
147
|
+
// Typing on a focused row: redirect the character (or Backspace) into the
|
|
148
|
+
// field. Space is left alone — on a row it activates the item, which is
|
|
149
|
+
// what a menu user expects once they have arrowed down.
|
|
150
|
+
const onKeyDownCapture = useCallback((e: ReactKeyboardEvent<HTMLElement>) => {
|
|
151
|
+
const handle = handleRef.current
|
|
152
|
+
if (!handle?.input) return
|
|
153
|
+
if (e.target === handle.input) return
|
|
154
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return
|
|
155
|
+
if (e.key.length === 1 && e.key !== ' ') {
|
|
156
|
+
e.preventDefault()
|
|
157
|
+
e.stopPropagation()
|
|
158
|
+
handle.input.focus()
|
|
159
|
+
handle.append(e.key)
|
|
160
|
+
} else if (e.key === 'Backspace') {
|
|
161
|
+
e.preventDefault()
|
|
162
|
+
e.stopPropagation()
|
|
163
|
+
handle.input.focus()
|
|
164
|
+
handle.deleteBackward()
|
|
165
|
+
}
|
|
166
|
+
}, [])
|
|
167
|
+
|
|
168
|
+
/** Whether a search field is mounted in the popup right now. */
|
|
169
|
+
const hasSearch = useCallback(() => handleRef.current !== null, [])
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
host,
|
|
173
|
+
hasSearch,
|
|
174
|
+
searchMounted,
|
|
175
|
+
onKeyDownCapture,
|
|
176
|
+
isSearchField,
|
|
177
|
+
highlightFirst,
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// DropdownSearch
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
export interface DropdownSearchProps
|
|
186
|
+
extends Omit<InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'size' | 'defaultValue'> {
|
|
187
|
+
/** The query. Filter the MenuItems you render against it. Optional under `filter`, where the panel holds the query. */
|
|
188
|
+
value?: string
|
|
189
|
+
/** Called on every keystroke, and with `""` on close (see `clearOnClose`). Optional under `filter`. */
|
|
190
|
+
onValueChange?: (value: string) => void
|
|
191
|
+
/** Local addition. Let the surrounding panel hold the query and filter its own `Dropdown.Item` rows by `label`, re-indexing the survivors and showing `Dropdown.Empty` when none match. Defaults to `false`. */
|
|
192
|
+
filter?: boolean
|
|
193
|
+
/** Placeholder text. Defaults to `"Search…"`. */
|
|
194
|
+
placeholder?: string
|
|
195
|
+
/** Reset the query to "" when the popup closes (the field unmounts with
|
|
196
|
+
* it), so the menu reopens unfiltered. @default true */
|
|
197
|
+
clearOnClose?: boolean
|
|
198
|
+
/** Take focus when the popup opens. @default true */
|
|
199
|
+
autoFocus?: boolean
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const DropdownSearch = forwardRef<HTMLInputElement, DropdownSearchProps>(
|
|
203
|
+
(
|
|
204
|
+
{
|
|
205
|
+
value,
|
|
206
|
+
onValueChange,
|
|
207
|
+
filter = false,
|
|
208
|
+
placeholder = 'Search…',
|
|
209
|
+
clearOnClose = true,
|
|
210
|
+
autoFocus = true,
|
|
211
|
+
className,
|
|
212
|
+
onKeyDown,
|
|
213
|
+
...props
|
|
214
|
+
},
|
|
215
|
+
ref,
|
|
216
|
+
) => {
|
|
217
|
+
const SearchIcon = useIcon('search')
|
|
218
|
+
const sizeClasses = useSize()
|
|
219
|
+
const compact = sizeClasses.variant === 'compact'
|
|
220
|
+
const host = useContext(DropdownSearchHostContext)
|
|
221
|
+
// Outside a popup host (an inline panel) the field is simply open.
|
|
222
|
+
const open = host?.open ?? true
|
|
223
|
+
// `filter`: the panel is the source of truth, and the consumer's own
|
|
224
|
+
// `onValueChange` (if any) still hears every keystroke.
|
|
225
|
+
const panel = useContext(DropdownFilterContext)
|
|
226
|
+
const managed = filter ? panel : null
|
|
227
|
+
const query = managed ? managed.query : (value ?? '')
|
|
228
|
+
const setQuery = useCallback(
|
|
229
|
+
(next: string) => {
|
|
230
|
+
managed?.setQuery(next)
|
|
231
|
+
onValueChange?.(next)
|
|
232
|
+
},
|
|
233
|
+
[managed, onValueChange],
|
|
234
|
+
)
|
|
235
|
+
// The popup surface, painted explicitly: rows scroll underneath the sticky
|
|
236
|
+
// field, and the field's parent is often a display:contents group (the
|
|
237
|
+
// menu's radio group) that `bg-inherit` would resolve to transparent.
|
|
238
|
+
const surface = useSurface()
|
|
239
|
+
const inputRef = useRef<HTMLInputElement | null>(null)
|
|
240
|
+
|
|
241
|
+
// Latest value / callback for the imperative handle and the unmount
|
|
242
|
+
// cleanup, without re-registering on every keystroke.
|
|
243
|
+
const valueRef = useRef(query)
|
|
244
|
+
valueRef.current = query
|
|
245
|
+
const onValueChangeRef = useRef(setQuery)
|
|
246
|
+
onValueChangeRef.current = setQuery
|
|
247
|
+
const clearOnCloseRef = useRef(clearOnClose)
|
|
248
|
+
clearOnCloseRef.current = clearOnClose
|
|
249
|
+
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
if (!host) return
|
|
252
|
+
return host.register({
|
|
253
|
+
get input() {
|
|
254
|
+
return inputRef.current
|
|
255
|
+
},
|
|
256
|
+
append: text => onValueChangeRef.current(valueRef.current + text),
|
|
257
|
+
deleteBackward: () => onValueChangeRef.current(valueRef.current.slice(0, -1)),
|
|
258
|
+
})
|
|
259
|
+
}, [host])
|
|
260
|
+
|
|
261
|
+
// On every open (mount, or a reopen while the popup was still playing
|
|
262
|
+
// its exit): reset the query so the list starts unfiltered, then take
|
|
263
|
+
// focus. The popup's own initial focus (first row on a keyboard open,
|
|
264
|
+
// the panel on a pointer open) is queued a frame after mount; the field
|
|
265
|
+
// takes over one frame later so a menu with a search opens ready to type.
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
if (!open) return
|
|
268
|
+
if (clearOnCloseRef.current && valueRef.current !== '') {
|
|
269
|
+
onValueChangeRef.current('')
|
|
270
|
+
}
|
|
271
|
+
if (!autoFocus) return
|
|
272
|
+
let inner: number | undefined
|
|
273
|
+
const outer = requestAnimationFrame(() => {
|
|
274
|
+
inner = requestAnimationFrame(() => inputRef.current?.focus())
|
|
275
|
+
})
|
|
276
|
+
return () => {
|
|
277
|
+
cancelAnimationFrame(outer)
|
|
278
|
+
if (inner !== undefined) cancelAnimationFrame(inner)
|
|
279
|
+
}
|
|
280
|
+
}, [open, autoFocus])
|
|
281
|
+
|
|
282
|
+
// The rows re-filter in the same commit as the query, so after it the
|
|
283
|
+
// first enabled row is whatever Enter would pick now: highlight it while
|
|
284
|
+
// the field has focus. (Focus arriving at the field is the popup's
|
|
285
|
+
// onFocus; this covers the query moving under a focused field.)
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
if (!host || document.activeElement !== inputRef.current) return
|
|
288
|
+
host.highlightFirst()
|
|
289
|
+
}, [host, query])
|
|
290
|
+
|
|
291
|
+
// Unmount (the popup finished closing): reset the query so the next
|
|
292
|
+
// open shows the full list.
|
|
293
|
+
useEffect(
|
|
294
|
+
() => () => {
|
|
295
|
+
if (clearOnCloseRef.current && valueRef.current !== '') {
|
|
296
|
+
onValueChangeRef.current('')
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
[],
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
const handleKeyDown = (e: ReactKeyboardEvent<HTMLInputElement>) => {
|
|
303
|
+
onKeyDown?.(e)
|
|
304
|
+
if (e.defaultPrevented) return
|
|
305
|
+
// Escape and Tab belong to the menu (close / leave). Everything else is
|
|
306
|
+
// either typing, which the menu's typeahead must not see, or list
|
|
307
|
+
// navigation handled right here.
|
|
308
|
+
if (e.key === 'Escape' || e.key === 'Tab') return
|
|
309
|
+
e.stopPropagation()
|
|
310
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
311
|
+
const rows = menuRows(e.currentTarget)
|
|
312
|
+
if (rows.length === 0) return
|
|
313
|
+
e.preventDefault()
|
|
314
|
+
;(e.key === 'ArrowDown' ? rows[0] : rows[rows.length - 1])?.focus()
|
|
315
|
+
} else if (e.key === 'Enter') {
|
|
316
|
+
e.preventDefault()
|
|
317
|
+
menuRows(e.currentTarget)[0]?.click()
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<div
|
|
323
|
+
// Sticky at the top of the scrolling popup, bleeding into its 4px
|
|
324
|
+
// padding so the divider runs edge to edge and rows scroll underneath
|
|
325
|
+
// it; margin + the list gap put the first row 4px below the divider —
|
|
326
|
+
// the same inset as the popup's side padding.
|
|
327
|
+
className={cn(
|
|
328
|
+
'group/search sticky top-0 z-20 -mx-1 -mt-1 mb-0.5 flex shrink-0 items-center border-b border-border/60',
|
|
329
|
+
SURFACE_BG[surface],
|
|
330
|
+
sizeClasses.control,
|
|
331
|
+
sizeClasses.gap,
|
|
332
|
+
compact ? 'px-2.5' : 'px-3',
|
|
333
|
+
className,
|
|
334
|
+
)}
|
|
335
|
+
>
|
|
336
|
+
<SearchIcon
|
|
337
|
+
size={sizeClasses.icon}
|
|
338
|
+
strokeWidth={1.5}
|
|
339
|
+
className="shrink-0 text-muted-foreground transition-[color,stroke-width] duration-fast group-focus-within/search:text-foreground group-focus-within/search:stroke-[2]"
|
|
340
|
+
/>
|
|
341
|
+
<input
|
|
342
|
+
ref={node => {
|
|
343
|
+
inputRef.current = node
|
|
344
|
+
if (typeof ref === 'function') ref(node)
|
|
345
|
+
else if (ref) ref.current = node
|
|
346
|
+
}}
|
|
347
|
+
type="text"
|
|
348
|
+
role="searchbox"
|
|
349
|
+
autoComplete="off"
|
|
350
|
+
autoCorrect="off"
|
|
351
|
+
spellCheck={false}
|
|
352
|
+
value={query}
|
|
353
|
+
onChange={e => setQuery(e.target.value)}
|
|
354
|
+
onKeyDown={handleKeyDown}
|
|
355
|
+
placeholder={placeholder}
|
|
356
|
+
// rounded-none: the site's base :focus-visible rule hands focused
|
|
357
|
+
// elements the shape radius, and a text input clips its caret to
|
|
358
|
+
// its own corners (pill mode nicks the caret at the left edge).
|
|
359
|
+
className={cn(
|
|
360
|
+
'min-w-0 flex-1 rounded-none bg-transparent text-foreground placeholder:text-muted-foreground outline-none font-[inherit]',
|
|
361
|
+
sizeClasses.text,
|
|
362
|
+
// The line box: Safari runs the caret its full height, so the
|
|
363
|
+
// ladder's leading keeps it in proportion to the row. (Chrome
|
|
364
|
+
// sizes the caret to the font itself, whatever the leading.)
|
|
365
|
+
compact ? 'leading-5' : 'leading-6',
|
|
366
|
+
)}
|
|
367
|
+
{...props}
|
|
368
|
+
/>
|
|
369
|
+
</div>
|
|
370
|
+
)
|
|
371
|
+
},
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
DropdownSearch.displayName = 'DropdownSearch'
|
|
375
|
+
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
// DropdownEmpty — the "no results" row a filtered menu shows instead of items.
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
const DropdownEmpty = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => {
|
|
381
|
+
const sizeClasses = useSize()
|
|
382
|
+
return (
|
|
383
|
+
<div
|
|
384
|
+
ref={ref}
|
|
385
|
+
role="status"
|
|
386
|
+
aria-live="polite"
|
|
387
|
+
className={cn('px-2 py-6 text-center text-muted-foreground', sizeClasses.text, className)}
|
|
388
|
+
{...props}
|
|
389
|
+
/>
|
|
390
|
+
)
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
DropdownEmpty.displayName = 'DropdownEmpty'
|
|
394
|
+
|
|
395
|
+
export { DropdownEmpty, DropdownSearch }
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import Dropdown from './index'
|
|
2
|
+
import Button from '../../button'
|
|
3
|
+
import Dialog from '../dialog'
|
|
4
|
+
import { Bell, ChevronDown, Clock, Copy, Ellipsis, Lock, LogOut, Mail, Monitor, Palette, Pencil, Settings, Shield, SquareLibrary, Star, Trash2, User, Users } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
# Basic
|
|
7
|
+
|
|
8
|
+
The inline panel, running its own selection: `defaultCheckedIndex` makes
|
|
9
|
+
the rows radio options and the panel moves the marker itself.
|
|
10
|
+
|
|
11
|
+
<Dropdown defaultCheckedIndex={0}>
|
|
12
|
+
<Dropdown.Item index={0} icon={SquareLibrary} label="Teamspaces" />
|
|
13
|
+
<Dropdown.Item index={1} icon={Clock} label="Recents" />
|
|
14
|
+
<Dropdown.Item index={2} icon={Star} label="Favorites" />
|
|
15
|
+
<Dropdown.Item index={3} icon={Users} label="Shared" />
|
|
16
|
+
<Dropdown.Item index={4} icon={Lock} label="Private" />
|
|
17
|
+
</Dropdown>
|
|
18
|
+
|
|
19
|
+
# Groups
|
|
20
|
+
|
|
21
|
+
Without any checked state the rows are plain actions. Dropdown.Label
|
|
22
|
+
captions a run of them, Dropdown.Separator rules between the runs, and the
|
|
23
|
+
indices keep counting across both.
|
|
24
|
+
|
|
25
|
+
<Dropdown>
|
|
26
|
+
<Dropdown.Label>Account</Dropdown.Label>
|
|
27
|
+
<Dropdown.Item index={0} icon={Mail} label="Email" />
|
|
28
|
+
<Dropdown.Item index={1} icon={Bell} label="Notifications" />
|
|
29
|
+
<Dropdown.Item index={2} icon={Shield} label="Privacy" />
|
|
30
|
+
<Dropdown.Separator />
|
|
31
|
+
<Dropdown.Label>Appearance</Dropdown.Label>
|
|
32
|
+
<Dropdown.Item index={3} icon={Settings} label="General" />
|
|
33
|
+
<Dropdown.Item index={4} icon={Palette} label="Theme" />
|
|
34
|
+
<Dropdown.Item index={5} icon={Monitor} label="Display" />
|
|
35
|
+
</Dropdown>
|
|
36
|
+
|
|
37
|
+
# Triggered menu
|
|
38
|
+
|
|
39
|
+
3 parts: Dropdown.Menu, Dropdown.Trigger, Dropdown.Content. Any element
|
|
40
|
+
goes in `render`, and picking a row closes the menu.
|
|
41
|
+
|
|
42
|
+
<Dropdown.Menu>
|
|
43
|
+
<Dropdown.Trigger render={<Button variant="ghost">Open menu</Button>} />
|
|
44
|
+
<Dropdown.Content defaultCheckedIndex={0}>
|
|
45
|
+
<Dropdown.Item index={0} icon={SquareLibrary} label="Teamspaces" />
|
|
46
|
+
<Dropdown.Item index={1} icon={Clock} label="Recents" />
|
|
47
|
+
<Dropdown.Item index={2} icon={Star} label="Favorites" />
|
|
48
|
+
<Dropdown.Item index={3} icon={Users} label="Shared" />
|
|
49
|
+
<Dropdown.Item index={4} icon={Lock} label="Private" />
|
|
50
|
+
</Dropdown.Content>
|
|
51
|
+
</Dropdown.Menu>
|
|
52
|
+
|
|
53
|
+
# Morph
|
|
54
|
+
|
|
55
|
+
The menu oozes out of its trigger by default. `effect="morph"` with `hideSource` turns the trigger itself into the menu and shrinks it back, one continuous surface.
|
|
56
|
+
|
|
57
|
+
<div className="flex flex-wrap items-start justify-center gap-3">
|
|
58
|
+
<Dropdown.Menu>
|
|
59
|
+
<Dropdown.Trigger render={<Button variant="secondary">Goo</Button>} />
|
|
60
|
+
<Dropdown.Content defaultCheckedIndex={0}>
|
|
61
|
+
<Dropdown.Item index={0} icon={Mail} label="Email" />
|
|
62
|
+
<Dropdown.Item index={1} icon={Bell} label="Notifications" />
|
|
63
|
+
<Dropdown.Item index={2} icon={Shield} label="Privacy" />
|
|
64
|
+
</Dropdown.Content>
|
|
65
|
+
</Dropdown.Menu>
|
|
66
|
+
<Dropdown.Menu>
|
|
67
|
+
<Dropdown.Trigger render={<Button variant="secondary">Morph</Button>} />
|
|
68
|
+
<Dropdown.Content effect="morph" hideSource defaultCheckedIndex={0}>
|
|
69
|
+
<Dropdown.Item index={0} icon={Settings} label="General" />
|
|
70
|
+
<Dropdown.Item index={1} icon={Palette} label="Theme" />
|
|
71
|
+
<Dropdown.Item index={2} icon={Monitor} label="Display" />
|
|
72
|
+
</Dropdown.Content>
|
|
73
|
+
</Dropdown.Menu>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
# Multiple selection
|
|
77
|
+
|
|
78
|
+
`defaultCheckedIndices` (or `checkedIndices`): rows become checkboxes, the
|
|
79
|
+
menu stays open as you toggle, and touching picks share one background.
|
|
80
|
+
|
|
81
|
+
<Dropdown.Menu>
|
|
82
|
+
<Dropdown.Trigger render={<Button variant="ghost">Status</Button>} />
|
|
83
|
+
<Dropdown.Content defaultCheckedIndices={[0, 1, 2]}>
|
|
84
|
+
{['Open', 'In progress', 'In review', 'Blocked', 'Done', 'Archived'].map((status, i) => (
|
|
85
|
+
<Dropdown.Item key={status} index={i} label={status} />
|
|
86
|
+
))}
|
|
87
|
+
</Dropdown.Content>
|
|
88
|
+
</Dropdown.Menu>
|
|
89
|
+
|
|
90
|
+
# Searchable menu
|
|
91
|
+
|
|
92
|
+
Put a Dropdown.Search marked `filter` first and the panel filters its own
|
|
93
|
+
rows by `label`. Type to filter, press Enter to pick the first match.
|
|
94
|
+
|
|
95
|
+
<Dropdown.Menu>
|
|
96
|
+
<Dropdown.Trigger render={<Button variant="ghost" trailingIcon={ChevronDown}>Language</Button>} />
|
|
97
|
+
<Dropdown.Content defaultCheckedIndex={0}>
|
|
98
|
+
<Dropdown.Search filter placeholder="Search languages" />
|
|
99
|
+
{['Auto detect', 'Arabic', 'Bengali', 'Chinese (Simplified)', 'Czech', 'Danish', 'Dutch', 'English (UK)', 'English (US)', 'Finnish', 'French', 'German', 'Greek', 'Hebrew', 'Hindi', 'Hungarian', 'Indonesian', 'Italian', 'Japanese', 'Korean', 'Norwegian', 'Polish', 'Portuguese (Brazil)', 'Romanian', 'Russian', 'Spanish', 'Swedish', 'Thai', 'Turkish', 'Ukrainian', 'Vietnamese'].map((language, i) => (
|
|
100
|
+
<Dropdown.Item key={language} index={i} label={language} />
|
|
101
|
+
))}
|
|
102
|
+
<Dropdown.Empty>No languages found</Dropdown.Empty>
|
|
103
|
+
</Dropdown.Content>
|
|
104
|
+
</Dropdown.Menu>
|
|
105
|
+
|
|
106
|
+
# Searchable multiple selection
|
|
107
|
+
|
|
108
|
+
Both at once. The panel filters the rows and keeps the picks on the rows
|
|
109
|
+
themselves, so a filtered-away pick is still checked when it comes back.
|
|
110
|
+
|
|
111
|
+
<Dropdown.Menu>
|
|
112
|
+
<Dropdown.Trigger render={<Button variant="ghost">Labels</Button>} />
|
|
113
|
+
<Dropdown.Content defaultCheckedIndices={[0, 3]}>
|
|
114
|
+
<Dropdown.Search filter placeholder="Search labels" />
|
|
115
|
+
{['Bug', 'Feature', 'Docs', 'Design', 'Performance', 'Accessibility', 'Refactor', 'Testing', 'Infrastructure', 'Security', 'Good first issue', 'Help wanted'].map((label, i) => (
|
|
116
|
+
<Dropdown.Item key={label} index={i} label={label} />
|
|
117
|
+
))}
|
|
118
|
+
<Dropdown.Empty>No labels found</Dropdown.Empty>
|
|
119
|
+
</Dropdown.Content>
|
|
120
|
+
</Dropdown.Menu>
|
|
121
|
+
|
|
122
|
+
# Children items
|
|
123
|
+
|
|
124
|
+
Rows written shadcn-style: the content as children and no `index` — each row
|
|
125
|
+
takes its place in document order, conditional rows included. `shortcut`
|
|
126
|
+
adds a muted caption at the end, `inset` lines a text-only row up with the
|
|
127
|
+
rows that have an icon, and `onSelect` runs on a pick by pointer or keyboard.
|
|
128
|
+
|
|
129
|
+
<Dropdown.Menu>
|
|
130
|
+
<Dropdown.Trigger asChild>
|
|
131
|
+
<Button variant="ghost" size="icon" aria-label="Row actions"><Ellipsis /></Button>
|
|
132
|
+
</Dropdown.Trigger>
|
|
133
|
+
<Dropdown.Content align="end">
|
|
134
|
+
<Dropdown.Item icon={Pencil} shortcut="⌘E">Edit</Dropdown.Item>
|
|
135
|
+
<Dropdown.Item icon={Copy} shortcut="⌘D">Duplicate</Dropdown.Item>
|
|
136
|
+
<Dropdown.Item inset>Move to…</Dropdown.Item>
|
|
137
|
+
<Dropdown.Item disabled inset>Archive</Dropdown.Item>
|
|
138
|
+
<Dropdown.Separator />
|
|
139
|
+
<Dropdown.Item icon={Trash2} shortcut="⌫">Delete</Dropdown.Item>
|
|
140
|
+
</Dropdown.Content>
|
|
141
|
+
</Dropdown.Menu>
|
|
142
|
+
|
|
143
|
+
# Groups and labels
|
|
144
|
+
|
|
145
|
+
`Dropdown.Group` holds a run of related rows; a `Dropdown.Label` inside it
|
|
146
|
+
names the group for assistive tech. Labels and separators break the row
|
|
147
|
+
order, so nothing selected melts across them.
|
|
148
|
+
|
|
149
|
+
<Dropdown.Menu>
|
|
150
|
+
<Dropdown.Trigger render={<Button variant="secondary" leadingIcon={User}>Account</Button>} />
|
|
151
|
+
<Dropdown.Content>
|
|
152
|
+
<Dropdown.Group>
|
|
153
|
+
<Dropdown.Label>My account</Dropdown.Label>
|
|
154
|
+
<Dropdown.Item icon={User}>Profile</Dropdown.Item>
|
|
155
|
+
<Dropdown.Item icon={Settings}>Settings</Dropdown.Item>
|
|
156
|
+
</Dropdown.Group>
|
|
157
|
+
<Dropdown.Separator />
|
|
158
|
+
<Dropdown.Group>
|
|
159
|
+
<Dropdown.Label inset>Team</Dropdown.Label>
|
|
160
|
+
<Dropdown.Item inset>Invite members</Dropdown.Item>
|
|
161
|
+
<Dropdown.Item inset>New team</Dropdown.Item>
|
|
162
|
+
</Dropdown.Group>
|
|
163
|
+
<Dropdown.Separator />
|
|
164
|
+
<Dropdown.Item icon={LogOut}>Log out</Dropdown.Item>
|
|
165
|
+
</Dropdown.Content>
|
|
166
|
+
</Dropdown.Menu>
|
|
167
|
+
|
|
168
|
+
# Shortcut
|
|
169
|
+
|
|
170
|
+
`Dropdown.Shortcut` is the trailing caption on its own, for rows that lay
|
|
171
|
+
out their own content.
|
|
172
|
+
|
|
173
|
+
<Dropdown className="w-64">
|
|
174
|
+
<Dropdown.Item>
|
|
175
|
+
<Mail />
|
|
176
|
+
New message
|
|
177
|
+
<Dropdown.Shortcut>⌘N</Dropdown.Shortcut>
|
|
178
|
+
</Dropdown.Item>
|
|
179
|
+
<Dropdown.Item>
|
|
180
|
+
<Bell />
|
|
181
|
+
Mute thread
|
|
182
|
+
<Dropdown.Shortcut>⇧⌘M</Dropdown.Shortcut>
|
|
183
|
+
</Dropdown.Item>
|
|
184
|
+
</Dropdown>
|
|
185
|
+
|
|
186
|
+
# Submenu
|
|
187
|
+
|
|
188
|
+
A `Dropdown.Sub` holds a trigger row and its panel. ArrowRight, a press or a
|
|
189
|
+
resting pointer opens it, ArrowLeft or Escape closes it; it grows out of its
|
|
190
|
+
row, which stays lit while it is open, and nests as deep as you like.
|
|
191
|
+
|
|
192
|
+
<Dropdown.Menu>
|
|
193
|
+
<Dropdown.Trigger render={<Button variant="secondary">Options</Button>} />
|
|
194
|
+
<Dropdown.Content keepMounted>
|
|
195
|
+
<Dropdown.Item icon={User}>Profile</Dropdown.Item>
|
|
196
|
+
<Dropdown.Sub>
|
|
197
|
+
<Dropdown.SubTrigger icon={Palette}>Theme</Dropdown.SubTrigger>
|
|
198
|
+
<Dropdown.SubContent keepMounted>
|
|
199
|
+
<Dropdown.RadioGroup defaultValue="system">
|
|
200
|
+
<Dropdown.RadioItem value="light">Light</Dropdown.RadioItem>
|
|
201
|
+
<Dropdown.RadioItem value="dark">Dark</Dropdown.RadioItem>
|
|
202
|
+
<Dropdown.RadioItem value="system">System</Dropdown.RadioItem>
|
|
203
|
+
</Dropdown.RadioGroup>
|
|
204
|
+
</Dropdown.SubContent>
|
|
205
|
+
</Dropdown.Sub>
|
|
206
|
+
<Dropdown.Sub>
|
|
207
|
+
<Dropdown.SubTrigger icon={Users}>Invite users</Dropdown.SubTrigger>
|
|
208
|
+
<Dropdown.SubContent>
|
|
209
|
+
<Dropdown.Item icon={Mail}>Email</Dropdown.Item>
|
|
210
|
+
<Dropdown.Item icon={Copy}>Copy link</Dropdown.Item>
|
|
211
|
+
<Dropdown.Separator />
|
|
212
|
+
<Dropdown.Sub>
|
|
213
|
+
<Dropdown.SubTrigger inset>More</Dropdown.SubTrigger>
|
|
214
|
+
<Dropdown.SubContent>
|
|
215
|
+
<Dropdown.Item>Slack</Dropdown.Item>
|
|
216
|
+
<Dropdown.Item>Teams</Dropdown.Item>
|
|
217
|
+
</Dropdown.SubContent>
|
|
218
|
+
</Dropdown.Sub>
|
|
219
|
+
</Dropdown.SubContent>
|
|
220
|
+
</Dropdown.Sub>
|
|
221
|
+
<Dropdown.Separator />
|
|
222
|
+
<Dropdown.Item icon={LogOut}>Log out</Dropdown.Item>
|
|
223
|
+
</Dropdown.Content>
|
|
224
|
+
</Dropdown.Menu>
|
|
225
|
+
|
|
226
|
+
# Checkbox items
|
|
227
|
+
|
|
228
|
+
`Dropdown.CheckboxItem` toggles itself — `defaultChecked`, or `checked` with
|
|
229
|
+
`onCheckedChange` — and keeps the menu open. Touching checked rows share one
|
|
230
|
+
background, as in a multiple selection. Uncontrolled rows live inside the
|
|
231
|
+
popup, so `keepMounted` keeps their state across opens.
|
|
232
|
+
|
|
233
|
+
<Dropdown.Menu>
|
|
234
|
+
<Dropdown.Trigger render={<Button variant="secondary" trailingIcon={ChevronDown}>Columns</Button>} />
|
|
235
|
+
<Dropdown.Content align="end" keepMounted>
|
|
236
|
+
<Dropdown.Label>Visible columns</Dropdown.Label>
|
|
237
|
+
<Dropdown.CheckboxItem defaultChecked>Name</Dropdown.CheckboxItem>
|
|
238
|
+
<Dropdown.CheckboxItem defaultChecked>Email</Dropdown.CheckboxItem>
|
|
239
|
+
<Dropdown.CheckboxItem>Role</Dropdown.CheckboxItem>
|
|
240
|
+
<Dropdown.CheckboxItem defaultChecked>Status</Dropdown.CheckboxItem>
|
|
241
|
+
<Dropdown.CheckboxItem disabled>Created</Dropdown.CheckboxItem>
|
|
242
|
+
</Dropdown.Content>
|
|
243
|
+
</Dropdown.Menu>
|
|
244
|
+
|
|
245
|
+
# Radio group
|
|
246
|
+
|
|
247
|
+
`Dropdown.RadioGroup` picks one of its `Dropdown.RadioItem` rows by `value`
|
|
248
|
+
(`defaultValue`, or `value` with `onValueChange`). Picking one closes the
|
|
249
|
+
menu.
|
|
250
|
+
|
|
251
|
+
<Dropdown.Menu>
|
|
252
|
+
<Dropdown.Trigger render={<Button variant="secondary" trailingIcon={ChevronDown}>Language</Button>} />
|
|
253
|
+
<Dropdown.Content keepMounted>
|
|
254
|
+
<Dropdown.RadioGroup defaultValue="sk">
|
|
255
|
+
<Dropdown.Label>Language</Dropdown.Label>
|
|
256
|
+
<Dropdown.RadioItem value="sk">Slovenčina</Dropdown.RadioItem>
|
|
257
|
+
<Dropdown.RadioItem value="cs">Čeština</Dropdown.RadioItem>
|
|
258
|
+
<Dropdown.RadioItem value="en">English</Dropdown.RadioItem>
|
|
259
|
+
<Dropdown.RadioItem value="de" disabled>Deutsch</Dropdown.RadioItem>
|
|
260
|
+
</Dropdown.RadioGroup>
|
|
261
|
+
</Dropdown.Content>
|
|
262
|
+
</Dropdown.Menu>
|
|
263
|
+
|
|
264
|
+
# Inside a dialog
|
|
265
|
+
|
|
266
|
+
A menu lifts 2 surface levels above whatever it opens over — a dialog
|
|
267
|
+
included — and so does its submenu above the menu.
|
|
268
|
+
|
|
269
|
+
<Dialog>
|
|
270
|
+
<Dialog.Trigger render={<Button variant="tertiary">Open dialog</Button>} />
|
|
271
|
+
<Dialog.Content>
|
|
272
|
+
<Dialog.Header>
|
|
273
|
+
<Dialog.Title>Share document</Dialog.Title>
|
|
274
|
+
<Dialog.Description>Pick who can see it.</Dialog.Description>
|
|
275
|
+
</Dialog.Header>
|
|
276
|
+
<Dropdown.Menu>
|
|
277
|
+
<Dropdown.Trigger render={<Button variant="secondary" trailingIcon={ChevronDown}>Access</Button>} />
|
|
278
|
+
<Dropdown.Content keepMounted>
|
|
279
|
+
<Dropdown.RadioGroup defaultValue="team">
|
|
280
|
+
<Dropdown.RadioItem value="private" icon={Lock}>Only me</Dropdown.RadioItem>
|
|
281
|
+
<Dropdown.RadioItem value="team" icon={Users}>My team</Dropdown.RadioItem>
|
|
282
|
+
</Dropdown.RadioGroup>
|
|
283
|
+
<Dropdown.Separator />
|
|
284
|
+
<Dropdown.Sub>
|
|
285
|
+
<Dropdown.SubTrigger icon={Shield}>Advanced</Dropdown.SubTrigger>
|
|
286
|
+
<Dropdown.SubContent keepMounted>
|
|
287
|
+
<Dropdown.CheckboxItem defaultChecked>Allow comments</Dropdown.CheckboxItem>
|
|
288
|
+
<Dropdown.CheckboxItem>Allow downloads</Dropdown.CheckboxItem>
|
|
289
|
+
</Dropdown.SubContent>
|
|
290
|
+
</Dropdown.Sub>
|
|
291
|
+
</Dropdown.Content>
|
|
292
|
+
</Dropdown.Menu>
|
|
293
|
+
</Dialog.Content>
|
|
294
|
+
</Dialog>
|