@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
|
+
/*
|
|
2
|
+
* Local addition (not part of Fluid Functionalism): the field and panel every
|
|
3
|
+
* date and time picker shares. Not an item (a file in a group folder): the
|
|
4
|
+
* pickers import it.
|
|
5
|
+
*
|
|
6
|
+
* The field is a text field on the shared field box (`useFieldBox` in
|
|
7
|
+
* `lib/field-classes.ts`, `bordered` unless the picker's `variant` says
|
|
8
|
+
* otherwise) showing the formatted value. Pressed, it opens its panel through Base UI's
|
|
9
|
+
* Popover and the morph layer, with Combobox's options: goo out of the field
|
|
10
|
+
* by default, or in place over it (`effect="morph"` with `hideSource`), the
|
|
11
|
+
* panel's header landing where the field was. On a phone-width screen the
|
|
12
|
+
* panel is the Sheet item instead, from the bottom edge.
|
|
13
|
+
*
|
|
14
|
+
* With an `editor` (DatePicker's `editable`) the field is a box around that
|
|
15
|
+
* text control instead of a button, and only its leading icon, an
|
|
16
|
+
* `InputAction`, opens the panel; the panel still anchors to and grows out
|
|
17
|
+
* of the whole field. `readOnly` shows the value and never opens.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { Popover } from '@base-ui/react/popover'
|
|
21
|
+
import {
|
|
22
|
+
type ButtonHTMLAttributes,
|
|
23
|
+
forwardRef,
|
|
24
|
+
type ReactElement,
|
|
25
|
+
type ReactNode,
|
|
26
|
+
type RefObject,
|
|
27
|
+
useCallback,
|
|
28
|
+
useRef,
|
|
29
|
+
useState,
|
|
30
|
+
} from 'react'
|
|
31
|
+
import { type FieldVariant, fieldIconClasses, useFieldBox } from '../../../lib/field-classes'
|
|
32
|
+
import { type IconName, useIcon } from '../../../lib/icon-context'
|
|
33
|
+
import { MorphSurface } from '../../../lib/morph-layers'
|
|
34
|
+
import { useShape } from '../../../lib/shape-context'
|
|
35
|
+
import { type SizeVariant, useSize } from '../../../lib/size-context'
|
|
36
|
+
import { SURFACE_BG, SURFACE_SHADOW } from '../../../lib/surface-classes'
|
|
37
|
+
import { SurfaceProvider, useSurface } from '../../../lib/surface-context'
|
|
38
|
+
import { useControllableState } from '../../../lib/use-controllable-state'
|
|
39
|
+
import { useIsMobile } from '../../../lib/use-is-mobile'
|
|
40
|
+
import {
|
|
41
|
+
inPlaceOffset,
|
|
42
|
+
type MorphEffect,
|
|
43
|
+
type MorphFrom,
|
|
44
|
+
type MorphTier,
|
|
45
|
+
opensInPlace,
|
|
46
|
+
useMorph,
|
|
47
|
+
useMorphOrigin,
|
|
48
|
+
} from '../../../lib/use-morph'
|
|
49
|
+
import { cn } from '../../../lib/utils'
|
|
50
|
+
import { SizeProvider } from '../../../primitives/sizes'
|
|
51
|
+
import Button from '../../button'
|
|
52
|
+
import Sheet from '../../overlays/sheet'
|
|
53
|
+
import Input, { InputAction, InputAdornment, type InputProps } from '../input'
|
|
54
|
+
|
|
55
|
+
interface PickerShellProps {
|
|
56
|
+
open?: boolean
|
|
57
|
+
defaultOpen?: boolean
|
|
58
|
+
onOpenChange?: (open: boolean) => void
|
|
59
|
+
size?: SizeVariant
|
|
60
|
+
variant?: FieldVariant
|
|
61
|
+
invalid?: boolean
|
|
62
|
+
disabled?: boolean
|
|
63
|
+
/** The field's leading icon. */
|
|
64
|
+
icon: IconName
|
|
65
|
+
/** The formatted value; empty shows the placeholder. */
|
|
66
|
+
display: ReactNode
|
|
67
|
+
placeholder?: string
|
|
68
|
+
/** The panel header's inputs; the shell adds the close button after them. */
|
|
69
|
+
header: ReactNode
|
|
70
|
+
/** What the panel focuses when it opens on a desktop (its header input). */
|
|
71
|
+
initialFocus?: RefObject<HTMLElement | null>
|
|
72
|
+
/** Accessible name of the close button. */
|
|
73
|
+
closeLabel?: string
|
|
74
|
+
id?: string
|
|
75
|
+
dir?: string
|
|
76
|
+
/** Classes for the field. */
|
|
77
|
+
className?: string
|
|
78
|
+
/** Classes for the panel on a desktop: its width. */
|
|
79
|
+
popupClassName?: string
|
|
80
|
+
/** The desktop panel's morph (see Morph); a phone's sheet grows from its edge. */
|
|
81
|
+
from?: MorphFrom
|
|
82
|
+
effect?: MorphEffect
|
|
83
|
+
hideSource?: boolean
|
|
84
|
+
tier?: MorphTier
|
|
85
|
+
/** The field's own text control (an editable picker): the field becomes a box around it, and its icon a button that opens the panel. */
|
|
86
|
+
editor?: ReactNode
|
|
87
|
+
/** Accessible name of that icon button. Defaults to `"Open calendar"`. */
|
|
88
|
+
triggerLabel?: string
|
|
89
|
+
/** Shows the value and never opens. */
|
|
90
|
+
readOnly?: boolean
|
|
91
|
+
/** The panel body under the header. */
|
|
92
|
+
children: ReactNode
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The field and its panel: a popover out of the field (or in place of it), or a bottom sheet on a phone. */
|
|
96
|
+
function PickerShell({ open: openProp, defaultOpen = false, onOpenChange, size, ...props }: PickerShellProps) {
|
|
97
|
+
const [open, setOpenState] = useControllableState(openProp, defaultOpen, onOpenChange)
|
|
98
|
+
const setOpen = (next: boolean) => {
|
|
99
|
+
if (next && props.readOnly) return
|
|
100
|
+
setOpenState(next)
|
|
101
|
+
}
|
|
102
|
+
const mobile = useIsMobile(640)
|
|
103
|
+
const shell = mobile ? (
|
|
104
|
+
<PickerSheet open={open} setOpen={setOpen} {...props} />
|
|
105
|
+
) : (
|
|
106
|
+
<PickerPopover open={open} setOpen={setOpen} {...props} />
|
|
107
|
+
)
|
|
108
|
+
// A size pins the field and the portalled panel (context crosses portals).
|
|
109
|
+
return size ? <SizeProvider size={size}>{shell}</SizeProvider> : shell
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type PanelProps = Omit<PickerShellProps, 'open' | 'defaultOpen' | 'onOpenChange' | 'size'> & {
|
|
113
|
+
open: boolean
|
|
114
|
+
setOpen: (open: boolean) => void
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type PickerFieldProps = Pick<
|
|
118
|
+
PanelProps,
|
|
119
|
+
'icon' | 'display' | 'placeholder' | 'variant' | 'invalid' | 'disabled' | 'readOnly' | 'id' | 'className'
|
|
120
|
+
> &
|
|
121
|
+
Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className'>
|
|
122
|
+
|
|
123
|
+
// Forwards its ref: Base UI anchors the panel to the field through it.
|
|
124
|
+
const PickerField = forwardRef<HTMLButtonElement, PickerFieldProps>(
|
|
125
|
+
({ icon, display, placeholder, variant = 'bordered', invalid, disabled, readOnly, id, className, ...props }, ref) => {
|
|
126
|
+
const Icon = useIcon(icon)
|
|
127
|
+
const { size: sizeClasses, props: boxProps } = useFieldBox({ variant, invalid, disabled, readOnly })
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<button
|
|
131
|
+
ref={ref}
|
|
132
|
+
type="button"
|
|
133
|
+
id={id}
|
|
134
|
+
disabled={disabled}
|
|
135
|
+
aria-invalid={invalid || undefined}
|
|
136
|
+
{...boxProps}
|
|
137
|
+
// Base UI's trigger props (open state, handlers, aria) arrive here.
|
|
138
|
+
{...props}
|
|
139
|
+
className={cn(
|
|
140
|
+
boxProps.className,
|
|
141
|
+
'flex w-full min-w-0 items-center text-left cursor-pointer outline-none focus-visible:ring-focus-ring',
|
|
142
|
+
sizeClasses.gap,
|
|
143
|
+
sizeClasses.control,
|
|
144
|
+
className,
|
|
145
|
+
)}
|
|
146
|
+
>
|
|
147
|
+
<Icon size={sizeClasses.icon} strokeWidth={1.5} className={fieldIconClasses} />
|
|
148
|
+
<span
|
|
149
|
+
className={cn('flex-1 truncate', sizeClasses.text, display ? 'text-foreground' : 'text-muted-foreground')}
|
|
150
|
+
>
|
|
151
|
+
{display || placeholder}
|
|
152
|
+
</span>
|
|
153
|
+
</button>
|
|
154
|
+
)
|
|
155
|
+
},
|
|
156
|
+
)
|
|
157
|
+
PickerField.displayName = 'PickerField'
|
|
158
|
+
|
|
159
|
+
type EditableFieldProps = Pick<
|
|
160
|
+
PanelProps,
|
|
161
|
+
'icon' | 'variant' | 'invalid' | 'disabled' | 'readOnly' | 'className' | 'editor' | 'triggerLabel'
|
|
162
|
+
> & {
|
|
163
|
+
/** Wraps the icon button in the panel's trigger. */
|
|
164
|
+
trigger: (button: ReactNode) => ReactNode
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** An editable picker's field: its text control, led by the icon button that opens the panel. */
|
|
168
|
+
const EditableField = forwardRef<HTMLDivElement, EditableFieldProps>(
|
|
169
|
+
(
|
|
170
|
+
{
|
|
171
|
+
icon,
|
|
172
|
+
variant = 'bordered',
|
|
173
|
+
invalid,
|
|
174
|
+
disabled,
|
|
175
|
+
readOnly,
|
|
176
|
+
className,
|
|
177
|
+
editor,
|
|
178
|
+
triggerLabel = 'Open calendar',
|
|
179
|
+
trigger,
|
|
180
|
+
},
|
|
181
|
+
ref,
|
|
182
|
+
) => {
|
|
183
|
+
const Icon = useIcon(icon)
|
|
184
|
+
const { size: sizeClasses, props: boxProps } = useFieldBox({ variant, invalid, disabled, readOnly })
|
|
185
|
+
return (
|
|
186
|
+
<div
|
|
187
|
+
ref={ref}
|
|
188
|
+
{...boxProps}
|
|
189
|
+
className={cn(
|
|
190
|
+
boxProps.className,
|
|
191
|
+
'flex w-full min-w-0 items-center',
|
|
192
|
+
sizeClasses.gap,
|
|
193
|
+
sizeClasses.control,
|
|
194
|
+
className,
|
|
195
|
+
)}
|
|
196
|
+
>
|
|
197
|
+
<InputAdornment>
|
|
198
|
+
{trigger(<InputAction label={triggerLabel} icon={Icon} disabled={disabled || readOnly} />)}
|
|
199
|
+
</InputAdornment>
|
|
200
|
+
{editor}
|
|
201
|
+
</div>
|
|
202
|
+
)
|
|
203
|
+
},
|
|
204
|
+
)
|
|
205
|
+
EditableField.displayName = 'EditableField'
|
|
206
|
+
|
|
207
|
+
function PanelHeader({
|
|
208
|
+
header,
|
|
209
|
+
closeLabel = 'Close',
|
|
210
|
+
close,
|
|
211
|
+
}: {
|
|
212
|
+
header: ReactNode
|
|
213
|
+
closeLabel?: string
|
|
214
|
+
close: () => void
|
|
215
|
+
}) {
|
|
216
|
+
const X = useIcon('x')
|
|
217
|
+
const sizeClasses = useSize()
|
|
218
|
+
return (
|
|
219
|
+
<div className="flex items-center gap-1 border-b border-border pb-1">
|
|
220
|
+
<div className="flex min-w-0 flex-1 items-center">{header}</div>
|
|
221
|
+
<Button variant="ghost" size="icon-sm" aria-label={closeLabel} onClick={close}>
|
|
222
|
+
<X size={sizeClasses.icon} strokeWidth={1.5} />
|
|
223
|
+
</Button>
|
|
224
|
+
</div>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function PickerPopover({
|
|
229
|
+
open,
|
|
230
|
+
setOpen,
|
|
231
|
+
header,
|
|
232
|
+
initialFocus,
|
|
233
|
+
closeLabel,
|
|
234
|
+
dir,
|
|
235
|
+
popupClassName,
|
|
236
|
+
from,
|
|
237
|
+
effect,
|
|
238
|
+
hideSource,
|
|
239
|
+
tier = 'moderate',
|
|
240
|
+
children,
|
|
241
|
+
editor,
|
|
242
|
+
triggerLabel,
|
|
243
|
+
...field
|
|
244
|
+
}: PanelProps) {
|
|
245
|
+
const fieldRef = useRef<HTMLDivElement>(null)
|
|
246
|
+
const { origin, capture } = useMorphOrigin()
|
|
247
|
+
const morph = useMorph(open, origin, { from, effect, hideSource, tier })
|
|
248
|
+
const shape = useShape()
|
|
249
|
+
const level = Math.min(useSurface() + 2, 8)
|
|
250
|
+
|
|
251
|
+
const handleOpenChange = useCallback(
|
|
252
|
+
(next: boolean, details: { reason?: string; trigger?: Element; event?: Event }) => {
|
|
253
|
+
// Pressing or focusing the editable field's own text is no reason to
|
|
254
|
+
// close: the panel belongs to that field.
|
|
255
|
+
if (!next && editor && (details.reason === 'outside-press' || details.reason === 'focus-out')) {
|
|
256
|
+
const event = details.event as (Event & { relatedTarget?: EventTarget | null }) | undefined
|
|
257
|
+
const target = (event?.relatedTarget ?? event?.target) as Node | null | undefined
|
|
258
|
+
if (target && fieldRef.current?.contains(target)) return
|
|
259
|
+
}
|
|
260
|
+
// An editable field's panel grows out of the whole field, not its icon.
|
|
261
|
+
if (next) capture(editor ? { event: details.event, trigger: fieldRef.current ?? undefined } : details)
|
|
262
|
+
setOpen(next)
|
|
263
|
+
},
|
|
264
|
+
[capture, setOpen, editor],
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
// Non-modal: the page keeps scrolling and the panel follows its field.
|
|
269
|
+
<Popover.Root open={open} onOpenChange={handleOpenChange} modal={false}>
|
|
270
|
+
{editor ? (
|
|
271
|
+
<EditableField
|
|
272
|
+
ref={fieldRef}
|
|
273
|
+
{...field}
|
|
274
|
+
editor={editor}
|
|
275
|
+
triggerLabel={triggerLabel}
|
|
276
|
+
trigger={button => <Popover.Trigger disabled={field.disabled} render={button as ReactElement} />}
|
|
277
|
+
/>
|
|
278
|
+
) : (
|
|
279
|
+
<Popover.Trigger disabled={field.disabled} render={<PickerField {...field} />} />
|
|
280
|
+
)}
|
|
281
|
+
<Popover.Portal>
|
|
282
|
+
<Popover.Positioner
|
|
283
|
+
anchor={editor ? fieldRef : undefined}
|
|
284
|
+
side="bottom"
|
|
285
|
+
align="start"
|
|
286
|
+
sideOffset={opensInPlace({ effect, hideSource }) ? inPlaceOffset : 6}
|
|
287
|
+
className="z-50 outline-none"
|
|
288
|
+
>
|
|
289
|
+
<Popover.Popup ref={morph.popupRef} initialFocus={initialFocus} className="relative outline-none">
|
|
290
|
+
<SurfaceProvider value={level}>
|
|
291
|
+
<MorphSurface
|
|
292
|
+
morph={morph}
|
|
293
|
+
bg={SURFACE_BG[level]}
|
|
294
|
+
shadow={SURFACE_SHADOW[3]}
|
|
295
|
+
radius={shape.container}
|
|
296
|
+
className={cn('flex max-w-[calc(100vw-1rem)] flex-col gap-1 p-1', popupClassName)}
|
|
297
|
+
>
|
|
298
|
+
<div dir={dir} className="flex min-h-0 flex-col gap-1">
|
|
299
|
+
<PanelHeader header={header} closeLabel={closeLabel} close={() => setOpen(false)} />
|
|
300
|
+
{children}
|
|
301
|
+
</div>
|
|
302
|
+
</MorphSurface>
|
|
303
|
+
</SurfaceProvider>
|
|
304
|
+
</Popover.Popup>
|
|
305
|
+
</Popover.Positioner>
|
|
306
|
+
</Popover.Portal>
|
|
307
|
+
</Popover.Root>
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function PickerSheet({ open, setOpen, header, closeLabel, dir, children, editor, triggerLabel, ...field }: PanelProps) {
|
|
312
|
+
const {
|
|
313
|
+
initialFocus: _focus,
|
|
314
|
+
popupClassName: _width,
|
|
315
|
+
from: _from,
|
|
316
|
+
effect: _effect,
|
|
317
|
+
hideSource: _hide,
|
|
318
|
+
tier: _tier,
|
|
319
|
+
...fieldProps
|
|
320
|
+
} = field
|
|
321
|
+
return (
|
|
322
|
+
<Sheet open={open} onOpenChange={setOpen}>
|
|
323
|
+
{editor ? (
|
|
324
|
+
<EditableField
|
|
325
|
+
{...fieldProps}
|
|
326
|
+
editor={editor}
|
|
327
|
+
triggerLabel={triggerLabel}
|
|
328
|
+
trigger={button => <Sheet.Trigger disabled={field.disabled} render={button as ReactElement} />}
|
|
329
|
+
/>
|
|
330
|
+
) : (
|
|
331
|
+
<Sheet.Trigger disabled={field.disabled} render={<PickerField {...fieldProps} />} />
|
|
332
|
+
)}
|
|
333
|
+
<Sheet.Content className="gap-2 p-3 pt-3">
|
|
334
|
+
<div dir={dir} className="flex min-h-0 flex-col gap-2">
|
|
335
|
+
<PanelHeader header={header} closeLabel={closeLabel} close={() => setOpen(false)} />
|
|
336
|
+
{children}
|
|
337
|
+
</div>
|
|
338
|
+
</Sheet.Content>
|
|
339
|
+
</Sheet>
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
interface PickerInputProps extends Omit<InputProps, 'value' | 'defaultValue' | 'onChange'> {
|
|
344
|
+
/** The value as the field shows it, while nothing is being typed. */
|
|
345
|
+
formatted: string
|
|
346
|
+
/** Reads typed text and commits it; returns whether it could. */
|
|
347
|
+
onText: (text: string) => boolean
|
|
348
|
+
/** Enter, after a valid entry: the pickers close. */
|
|
349
|
+
onEnter?: () => void
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* A header input: shows the formatted value, selects it all on focus, and
|
|
354
|
+
* commits what is typed as soon as it reads as a value (marking it invalid
|
|
355
|
+
* until then). Leaving it restores the formatted value. Hover and focus fill
|
|
356
|
+
* it without the field ring.
|
|
357
|
+
*/
|
|
358
|
+
const PickerInput = forwardRef<HTMLInputElement, PickerInputProps>(
|
|
359
|
+
({ formatted, onText, onEnter, onFocus, onBlur, onKeyDown, className, ...props }, ref) => {
|
|
360
|
+
const [draft, setDraft] = useState<string | null>(null)
|
|
361
|
+
const [invalid, setInvalid] = useState(false)
|
|
362
|
+
return (
|
|
363
|
+
<Input
|
|
364
|
+
ref={ref}
|
|
365
|
+
{...props}
|
|
366
|
+
// The header is already framed by the panel: hover and focus change
|
|
367
|
+
// the fill only (an invalid entry keeps its destructive ring).
|
|
368
|
+
className={cn('hover:ring-transparent focus-within:ring-transparent data-[active]:ring-transparent', className)}
|
|
369
|
+
value={draft ?? formatted}
|
|
370
|
+
invalid={invalid}
|
|
371
|
+
onChange={text => {
|
|
372
|
+
setDraft(text)
|
|
373
|
+
setInvalid(!onText(text))
|
|
374
|
+
}}
|
|
375
|
+
onFocus={e => {
|
|
376
|
+
e.currentTarget.select()
|
|
377
|
+
onFocus?.(e)
|
|
378
|
+
}}
|
|
379
|
+
onBlur={e => {
|
|
380
|
+
setDraft(null)
|
|
381
|
+
setInvalid(false)
|
|
382
|
+
onBlur?.(e)
|
|
383
|
+
}}
|
|
384
|
+
onKeyDown={e => {
|
|
385
|
+
if (e.key === 'Enter' && !invalid) onEnter?.()
|
|
386
|
+
onKeyDown?.(e)
|
|
387
|
+
}}
|
|
388
|
+
/>
|
|
389
|
+
)
|
|
390
|
+
},
|
|
391
|
+
)
|
|
392
|
+
PickerInput.displayName = 'PickerInput'
|
|
393
|
+
|
|
394
|
+
export type { PickerShellProps }
|
|
395
|
+
export { PickerInput, PickerShell }
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Local addition (not part of Fluid Functionalism): the hour column (its
|
|
3
|
+
* AM/PM folded in on a 12-hour clock) and the minute column the date picker
|
|
4
|
+
* (with time) and the time picker share. Not an item (a file in a group folder).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { format, type Locale } from 'date-fns'
|
|
8
|
+
import { useMemo } from 'react'
|
|
9
|
+
import { fieldLabel, type HourCycle } from '../../../lib/date-locale'
|
|
10
|
+
import { ScrollColumn } from '../../../lib/scroll-column'
|
|
11
|
+
import { cn } from '../../../lib/utils'
|
|
12
|
+
|
|
13
|
+
interface Time {
|
|
14
|
+
hours: number
|
|
15
|
+
minutes: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface TimeColumnsProps {
|
|
19
|
+
value: Time | null
|
|
20
|
+
onValueChange: (time: Time) => void
|
|
21
|
+
hourCycle: HourCycle
|
|
22
|
+
minuteStep: number
|
|
23
|
+
locale: Locale
|
|
24
|
+
orientation: 'vertical' | 'horizontal'
|
|
25
|
+
/** Columns share the row's width equally instead of keeping their narrow fixed widths. Defaults to `false`. */
|
|
26
|
+
fill?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const pad = (n: number) => String(n).padStart(2, '0')
|
|
30
|
+
|
|
31
|
+
/** Columns for the hour (with its AM/PM on a 12-hour clock) and the minute; a row of them divides itself. */
|
|
32
|
+
function TimeColumns({
|
|
33
|
+
value,
|
|
34
|
+
onValueChange,
|
|
35
|
+
hourCycle,
|
|
36
|
+
minuteStep,
|
|
37
|
+
locale,
|
|
38
|
+
orientation,
|
|
39
|
+
fill = false,
|
|
40
|
+
}: TimeColumnsProps) {
|
|
41
|
+
const twelve = hourCycle === 'h12' || hourCycle === 'h11'
|
|
42
|
+
const hours = value?.hours ?? null
|
|
43
|
+
const minutes = value?.minutes ?? 0
|
|
44
|
+
|
|
45
|
+
const hourOptions = useMemo(
|
|
46
|
+
() =>
|
|
47
|
+
Array.from({ length: 24 }, (_, h) => ({
|
|
48
|
+
value: h,
|
|
49
|
+
// "12 AM" … "11 PM" (h11: "0 AM" … "11 PM"), in the locale's words.
|
|
50
|
+
label: twelve ? format(new Date(2000, 0, 1, h), hourCycle === 'h11' ? 'K a' : 'h a', { locale }) : pad(h),
|
|
51
|
+
})),
|
|
52
|
+
[twelve, hourCycle, locale],
|
|
53
|
+
)
|
|
54
|
+
const minuteOptions = useMemo(
|
|
55
|
+
() =>
|
|
56
|
+
Array.from({ length: Math.ceil(60 / minuteStep) }, (_, i) => ({
|
|
57
|
+
value: i * minuteStep,
|
|
58
|
+
label: pad(i * minuteStep),
|
|
59
|
+
})),
|
|
60
|
+
[minuteStep],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const vertical = orientation === 'vertical'
|
|
64
|
+
// Beside a calendar: narrower than its month and year columns, an hour
|
|
65
|
+
// with its AM/PM a little wider. On their own (`fill`): an equal share.
|
|
66
|
+
const width = (fixed: string) => (!vertical ? 'w-full' : fill ? 'min-w-0 flex-1' : cn('shrink-0', fixed))
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<>
|
|
70
|
+
<ScrollColumn
|
|
71
|
+
label={fieldLabel(locale, 'hour')}
|
|
72
|
+
options={hourOptions}
|
|
73
|
+
value={hours}
|
|
74
|
+
onValueChange={h => onValueChange({ hours: h, minutes })}
|
|
75
|
+
orientation={orientation}
|
|
76
|
+
className={width(twelve ? 'w-15' : 'w-12')}
|
|
77
|
+
/>
|
|
78
|
+
<ScrollColumn
|
|
79
|
+
label={fieldLabel(locale, 'minute')}
|
|
80
|
+
options={minuteOptions}
|
|
81
|
+
value={hours === null ? null : minutes}
|
|
82
|
+
onValueChange={m => onValueChange({ hours: hours ?? 0, minutes: m })}
|
|
83
|
+
orientation={orientation}
|
|
84
|
+
className={width('w-12')}
|
|
85
|
+
/>
|
|
86
|
+
</>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type { Time }
|
|
91
|
+
export { pad, TimeColumns }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import TimePicker from './index'
|
|
2
|
+
import { de } from 'react-day-picker/locale'
|
|
3
|
+
|
|
4
|
+
# Basic
|
|
5
|
+
|
|
6
|
+
The locale's clock: in English, 12-hour, the hours read with AM/PM.
|
|
7
|
+
|
|
8
|
+
<div className="w-48 max-w-full">
|
|
9
|
+
<TimePicker defaultValue="15:33" />
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
# 24-hour clock
|
|
13
|
+
|
|
14
|
+
A German locale writes 24-hour times.
|
|
15
|
+
|
|
16
|
+
<div className="w-48 max-w-full">
|
|
17
|
+
<TimePicker locale={de} defaultValue="15:33" />
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
# Quarter hours
|
|
21
|
+
|
|
22
|
+
`minuteStep={15}` leaves four minutes to pick from.
|
|
23
|
+
|
|
24
|
+
<div className="w-48 max-w-full">
|
|
25
|
+
<TimePicker hourCycle="h23" minuteStep={15} placeholder="Start time" />
|
|
26
|
+
</div>
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Local addition (not part of Fluid Functionalism): a time-of-day field with
|
|
3
|
+
* scrolling hour and minute columns. Props after utegsk/ui's TimePicker
|
|
4
|
+
* (`hourCycle`, a string value), kept in line with the other pickers here.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { format, type Locale, set } from 'date-fns'
|
|
8
|
+
import { type RefObject, useRef } from 'react'
|
|
9
|
+
import { defaultLocale, type HourCycle, hourCycleOf, parseTimeText } from '../../../../lib/date-locale'
|
|
10
|
+
import type { FieldVariant } from '../../../../lib/field-classes'
|
|
11
|
+
import { dividedColumns } from '../../../../lib/scroll-column'
|
|
12
|
+
import type { SizeVariant } from '../../../../lib/size-context'
|
|
13
|
+
import { useControllableState } from '../../../../lib/use-controllable-state'
|
|
14
|
+
import { cn } from '../../../../lib/utils'
|
|
15
|
+
import { PickerInput, PickerShell } from '../picker-shell'
|
|
16
|
+
import { pad, type Time, TimeColumns } from '../time-columns'
|
|
17
|
+
|
|
18
|
+
interface TimePickerProps {
|
|
19
|
+
/** The picked time as `"HH:mm"` on a 24-hour clock (an `<input type="time">`'s value); `null` when empty. */
|
|
20
|
+
value?: string | null
|
|
21
|
+
/** Initial time, for an uncontrolled picker. Defaults to `null`. */
|
|
22
|
+
defaultValue?: string | null
|
|
23
|
+
/** Called with the new `"HH:mm"`, or `null` when the field is cleared. */
|
|
24
|
+
onValueChange?: (value: string | null) => void
|
|
25
|
+
/** The clock shown: on `'h12'` / `'h11'` the hours read with AM/PM. Defaults to the locale's. */
|
|
26
|
+
hourCycle?: HourCycle
|
|
27
|
+
/** Steps between the minute column's options. Defaults to `1`. */
|
|
28
|
+
minuteStep?: number
|
|
29
|
+
/** A react-day-picker / date-fns locale: the time's format and the AM/PM names. Defaults to `enUS`. */
|
|
30
|
+
locale?: Locale
|
|
31
|
+
/** Shown while empty. */
|
|
32
|
+
placeholder?: string
|
|
33
|
+
/** Disables the field. Defaults to `false`. */
|
|
34
|
+
disabled?: boolean
|
|
35
|
+
/** Framed at rest, or invisible until hovered or focused. Defaults to `"bordered"`. */
|
|
36
|
+
variant?: FieldVariant
|
|
37
|
+
/** Marks the field invalid. Defaults to `false`. */
|
|
38
|
+
invalid?: boolean
|
|
39
|
+
/** Pins the field and its panel to one step of the size ladder. Defaults to the surrounding SizeProvider. */
|
|
40
|
+
size?: SizeVariant
|
|
41
|
+
/** Controlled open state. */
|
|
42
|
+
open?: boolean
|
|
43
|
+
/** Initial open state, for an uncontrolled panel. Defaults to `false`. */
|
|
44
|
+
defaultOpen?: boolean
|
|
45
|
+
/** Called when the panel opens or closes. */
|
|
46
|
+
onOpenChange?: (open: boolean) => void
|
|
47
|
+
/** Id of the field, for a `Label`'s `htmlFor`. */
|
|
48
|
+
id?: string
|
|
49
|
+
/** Where the panel 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). */
|
|
50
|
+
from?: 'trigger' | 'pointer' | 'center' | 'top' | 'right' | 'bottom' | 'left' | RefObject<HTMLElement | null>
|
|
51
|
+
/** How the panel grows (see Morph), as Combobox's list does: with the liquid goo neck, a plain morph, a slide or a fade. `'morph'` with `hideSource` opens it in place over the field. Defaults to `'goo'`. */
|
|
52
|
+
effect?: 'goo' | 'morph' | 'slide' | 'fade'
|
|
53
|
+
/** Hide the field while open, so it reads as turning into the panel. Defaults to `false`. */
|
|
54
|
+
hideSource?: boolean
|
|
55
|
+
/** Spring tier of a plain morph, slide or fade; goo runs on its own `spring.goo`. Defaults to `'moderate'`. */
|
|
56
|
+
tier?: 'moderate' | 'slow'
|
|
57
|
+
/** Classes for the field. */
|
|
58
|
+
className?: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const toTime = (value: string | null): Time | null => {
|
|
62
|
+
const [hours, minutes] = value?.split(':').map(Number) ?? []
|
|
63
|
+
return hours === undefined || minutes === undefined ? null : { hours, minutes }
|
|
64
|
+
}
|
|
65
|
+
const toValue = ({ hours, minutes }: Time) => `${pad(hours)}:${pad(minutes)}`
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A time-of-day field with scrolling hour and minute columns.
|
|
69
|
+
*
|
|
70
|
+
* The panel opens with the time as text, selected and ready to type over
|
|
71
|
+
* ("15:33", "3:33 PM"), above an hour column and a minute column; on a
|
|
72
|
+
* 12-hour clock the hours read with AM/PM. Each column keeps its pick in the middle and
|
|
73
|
+
* marks it with a dot. The value is `"HH:mm"` on a 24-hour clock whatever the
|
|
74
|
+
* locale shows, the same as a native time input's.
|
|
75
|
+
*
|
|
76
|
+
* @example {@include ./examples.mdx}
|
|
77
|
+
*/
|
|
78
|
+
function TimePicker({
|
|
79
|
+
value: valueProp,
|
|
80
|
+
defaultValue = null,
|
|
81
|
+
onValueChange,
|
|
82
|
+
hourCycle,
|
|
83
|
+
minuteStep = 1,
|
|
84
|
+
locale = defaultLocale,
|
|
85
|
+
placeholder,
|
|
86
|
+
disabled,
|
|
87
|
+
variant,
|
|
88
|
+
invalid,
|
|
89
|
+
size,
|
|
90
|
+
open: openProp,
|
|
91
|
+
defaultOpen = false,
|
|
92
|
+
onOpenChange,
|
|
93
|
+
id,
|
|
94
|
+
from,
|
|
95
|
+
effect,
|
|
96
|
+
hideSource,
|
|
97
|
+
tier,
|
|
98
|
+
className,
|
|
99
|
+
}: TimePickerProps) {
|
|
100
|
+
const [value, setValue] = useControllableState(valueProp, defaultValue, onValueChange)
|
|
101
|
+
const [open, setOpen] = useControllableState(openProp, defaultOpen, onOpenChange)
|
|
102
|
+
const input = useRef<HTMLInputElement>(null)
|
|
103
|
+
const time = toTime(value)
|
|
104
|
+
const formatted = time ? format(set(new Date(), time), 'p', { locale }) : ''
|
|
105
|
+
|
|
106
|
+
const readTime = (text: string) => {
|
|
107
|
+
if (!text.trim()) {
|
|
108
|
+
setValue(null)
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
111
|
+
const read = parseTimeText(text, locale)
|
|
112
|
+
if (!read) return false
|
|
113
|
+
setValue(toValue({ hours: read.getHours(), minutes: read.getMinutes() }))
|
|
114
|
+
return true
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<PickerShell
|
|
119
|
+
open={open}
|
|
120
|
+
onOpenChange={setOpen}
|
|
121
|
+
size={size}
|
|
122
|
+
variant={variant}
|
|
123
|
+
invalid={invalid}
|
|
124
|
+
disabled={disabled}
|
|
125
|
+
icon="clock"
|
|
126
|
+
display={formatted}
|
|
127
|
+
placeholder={placeholder}
|
|
128
|
+
header={
|
|
129
|
+
<PickerInput
|
|
130
|
+
ref={input}
|
|
131
|
+
aria-label={placeholder ?? 'Time'}
|
|
132
|
+
placeholder={placeholder}
|
|
133
|
+
formatted={formatted}
|
|
134
|
+
onText={readTime}
|
|
135
|
+
onEnter={() => setOpen(false)}
|
|
136
|
+
className="flex-1"
|
|
137
|
+
/>
|
|
138
|
+
}
|
|
139
|
+
initialFocus={input}
|
|
140
|
+
popupClassName="w-[max(var(--anchor-width,0px),14rem)]"
|
|
141
|
+
id={id}
|
|
142
|
+
from={from}
|
|
143
|
+
effect={effect}
|
|
144
|
+
hideSource={hideSource}
|
|
145
|
+
tier={tier}
|
|
146
|
+
className={className}
|
|
147
|
+
>
|
|
148
|
+
<div className={cn(dividedColumns, 'h-56')}>
|
|
149
|
+
<TimeColumns
|
|
150
|
+
value={time}
|
|
151
|
+
onValueChange={next => setValue(toValue(next))}
|
|
152
|
+
hourCycle={hourCycle ?? hourCycleOf(locale)}
|
|
153
|
+
minuteStep={minuteStep}
|
|
154
|
+
locale={locale}
|
|
155
|
+
orientation="vertical"
|
|
156
|
+
fill
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
</PickerShell>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export type { TimePickerProps }
|
|
164
|
+
export { TimePicker }
|
|
165
|
+
|
|
166
|
+
export default TimePicker
|