@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,541 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Vendored from Fluid Functionalism — `registry/default/hooks/use-fluid-hover.ts` at
|
|
3
|
+
* github.com/mickadesign/fluid-functionalism@b3587bdbd83fc66c2a6aae3817ffb856cb09260b
|
|
4
|
+
* (fluidfunctionalism.com). MIT License © 2026 Micka Touillaud — see
|
|
5
|
+
* LICENSE.fluid-functionalism in this package.
|
|
6
|
+
*
|
|
7
|
+
* Local modifications:
|
|
8
|
+
* - `"use client"` directive dropped (no RSC here).
|
|
9
|
+
* - React 18 types: `sessionRef` typed `MutableRefObject<number>` (upstream
|
|
10
|
+
* targets React 19, where `RefObject.current` is mutable and non-null).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
type Dispatch,
|
|
15
|
+
type MutableRefObject,
|
|
16
|
+
type RefObject,
|
|
17
|
+
type SetStateAction,
|
|
18
|
+
useCallback,
|
|
19
|
+
useEffect,
|
|
20
|
+
useRef,
|
|
21
|
+
useState,
|
|
22
|
+
} from 'react'
|
|
23
|
+
|
|
24
|
+
export interface ItemRect {
|
|
25
|
+
top: number
|
|
26
|
+
height: number
|
|
27
|
+
left: number
|
|
28
|
+
width: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface UseFluidHoverOptions {
|
|
32
|
+
/**
|
|
33
|
+
* Which direction to resolve the nearest item along.
|
|
34
|
+
* "y" — vertical lists (default): closest by top/height
|
|
35
|
+
* "x" — horizontal strips: closest by left/width
|
|
36
|
+
* "xy" — 2-D grids: closest card across both rows AND columns,
|
|
37
|
+
* measured by Euclidean distance to each item's center
|
|
38
|
+
*/
|
|
39
|
+
axis?: 'x' | 'y' | 'xy'
|
|
40
|
+
/**
|
|
41
|
+
* Makes an item invisible to hit-testing without unregistering it — for
|
|
42
|
+
* rows that stay mounted while clipped away (a collapsed sub-tree).
|
|
43
|
+
* Unregistering would invalidate every measurement; a skipped item keeps
|
|
44
|
+
* the set stable. Consulted per mouse move, so keep it cheap.
|
|
45
|
+
*/
|
|
46
|
+
isItemDisabled?: (element: HTMLElement) => boolean
|
|
47
|
+
/**
|
|
48
|
+
* Whether a click that lands between items (a gap, the padding, past the
|
|
49
|
+
* last row) is routed to the highlighted item, so what is lit is what a
|
|
50
|
+
* click hits. On by default: in a menu or a list the highlight is a
|
|
51
|
+
* promise about the click. Pass `false` where empty space should stay
|
|
52
|
+
* inert (rows with destructive actions, generous whitespace), or
|
|
53
|
+
* `{ maxDistance }` to route only clicks within that many pixels of the
|
|
54
|
+
* highlighted item's edge.
|
|
55
|
+
*/
|
|
56
|
+
gapClick?: boolean | { maxDistance?: number }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface UseFluidHoverReturn {
|
|
60
|
+
activeIndex: number | null
|
|
61
|
+
setActiveIndex: Dispatch<SetStateAction<number | null>>
|
|
62
|
+
itemRects: ItemRect[]
|
|
63
|
+
/**
|
|
64
|
+
* True once every registered item has been measured and no remeasure is
|
|
65
|
+
* pending, i.e. `itemRects` describes the current item set. Gate absolutely
|
|
66
|
+
* positioned overlays on it: an overlay that mounts against a rect a later
|
|
67
|
+
* pass still corrects animates from the wrong place to the right one, which
|
|
68
|
+
* reads as the highlight sliding in from another row.
|
|
69
|
+
*/
|
|
70
|
+
isMeasured: boolean
|
|
71
|
+
sessionRef: MutableRefObject<number>
|
|
72
|
+
handlers: {
|
|
73
|
+
onMouseMove: (e: React.MouseEvent) => void
|
|
74
|
+
onMouseEnter: () => void
|
|
75
|
+
onMouseLeave: () => void
|
|
76
|
+
/**
|
|
77
|
+
* Routes a click that lands between items (a gap, the padding, past the
|
|
78
|
+
* last row) to the highlighted item, so the highlight and the click agree:
|
|
79
|
+
* what is lit is what a click hits. A click inside an item is left to the
|
|
80
|
+
* item. Disabled items (`isItemDisabled`) are never activated.
|
|
81
|
+
*/
|
|
82
|
+
onClick: (e: React.MouseEvent) => void
|
|
83
|
+
}
|
|
84
|
+
registerItem: (index: number, element: HTMLElement | null) => void
|
|
85
|
+
/**
|
|
86
|
+
* Invalidates the published rects and runs the hook's coalesced measurement
|
|
87
|
+
* pass again, holding `isMeasured` false until it settles. Reach for it when
|
|
88
|
+
* the rects may be wrong and showing an overlay against them would misplace
|
|
89
|
+
* it: a popup that stays mounted between opens keeps its items registered,
|
|
90
|
+
* so nothing else would notice that its rects were taken while it was
|
|
91
|
+
* hidden. Registration, item resize, and container resize already trigger
|
|
92
|
+
* a pass; do not call this on `children` changes.
|
|
93
|
+
*/
|
|
94
|
+
remeasure: () => void
|
|
95
|
+
/**
|
|
96
|
+
* Re-reads the rects synchronously, keeping `isMeasured` as it is. Only for
|
|
97
|
+
* layout that moves the rows under a visible overlay frame by frame (the
|
|
98
|
+
* accordion re-measures inside its height animation). Everything else
|
|
99
|
+
* wants `remeasure`, or nothing.
|
|
100
|
+
*/
|
|
101
|
+
measureItems: () => void
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface PickNearestInput {
|
|
105
|
+
axis: 'x' | 'y' | 'xy'
|
|
106
|
+
/** The pointer, in viewport coordinates. */
|
|
107
|
+
point: { x: number; y: number }
|
|
108
|
+
/** Item rects in the container's layout space (sparse: unregistered slots
|
|
109
|
+
* are undefined). */
|
|
110
|
+
rects: readonly (ItemRect | undefined)[]
|
|
111
|
+
/** The container's bounding rect and live scroll / border offsets, which
|
|
112
|
+
* map layout rects into the pointer's viewport space. */
|
|
113
|
+
containerRect: { left: number; top: number; width: number; height: number }
|
|
114
|
+
scroll: { x: number; y: number }
|
|
115
|
+
border: { x: number; y: number }
|
|
116
|
+
/** Layout size of the container, so a cumulative ancestor `transform:
|
|
117
|
+
* scale` (a popup mid scale-in) can be factored out per axis. */
|
|
118
|
+
layoutSize: { width: number; height: number }
|
|
119
|
+
/** Skips an item without unregistering it. */
|
|
120
|
+
isDisabled?: (index: number) => boolean
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The rule, as one pure function: an item the pointer is inside wins;
|
|
125
|
+
* otherwise the item whose center is nearest does, so a pointer in a gap, in
|
|
126
|
+
* the padding, or past the last row still lands. `y` and `x` measure one
|
|
127
|
+
* coordinate; `xy` measures the straight line to each center. Ties keep the
|
|
128
|
+
* first item. The hook calls this once per animation frame; the docs page
|
|
129
|
+
* times it.
|
|
130
|
+
*/
|
|
131
|
+
export function pickNearest({
|
|
132
|
+
axis,
|
|
133
|
+
point,
|
|
134
|
+
rects,
|
|
135
|
+
containerRect,
|
|
136
|
+
scroll,
|
|
137
|
+
border,
|
|
138
|
+
layoutSize,
|
|
139
|
+
isDisabled,
|
|
140
|
+
}: PickNearestInput): number | null {
|
|
141
|
+
const scaleX = layoutSize.width > 0 ? containerRect.width / layoutSize.width : 1
|
|
142
|
+
const scaleY = layoutSize.height > 0 ? containerRect.height / layoutSize.height : 1
|
|
143
|
+
let closestIndex: number | null = null
|
|
144
|
+
let closestDistance = Infinity
|
|
145
|
+
let containingIndex: number | null = null
|
|
146
|
+
|
|
147
|
+
for (let index = 0; index < rects.length; index++) {
|
|
148
|
+
const r = rects[index]
|
|
149
|
+
if (!r) continue
|
|
150
|
+
if (isDisabled?.(index)) continue
|
|
151
|
+
|
|
152
|
+
if (axis === 'xy') {
|
|
153
|
+
const left = containerRect.left + (border.x + r.left - scroll.x) * scaleX
|
|
154
|
+
const top = containerRect.top + (border.y + r.top - scroll.y) * scaleY
|
|
155
|
+
const width = r.width * scaleX
|
|
156
|
+
const height = r.height * scaleY
|
|
157
|
+
if (point.x >= left && point.x <= left + width && point.y >= top && point.y <= top + height) {
|
|
158
|
+
containingIndex = index
|
|
159
|
+
}
|
|
160
|
+
const distance = Math.hypot(point.x - (left + width / 2), point.y - (top + height / 2))
|
|
161
|
+
if (distance < closestDistance) {
|
|
162
|
+
closestDistance = distance
|
|
163
|
+
closestIndex = index
|
|
164
|
+
}
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const horizontal = axis === 'x'
|
|
169
|
+
const mousePos = horizontal ? point.x : point.y
|
|
170
|
+
const scale = horizontal ? scaleX : scaleY
|
|
171
|
+
const itemStart =
|
|
172
|
+
(horizontal ? containerRect.left : containerRect.top) +
|
|
173
|
+
((horizontal ? border.x : border.y) + (horizontal ? r.left : r.top) - (horizontal ? scroll.x : scroll.y)) * scale
|
|
174
|
+
const itemSize = (horizontal ? r.width : r.height) * scale
|
|
175
|
+
if (mousePos >= itemStart && mousePos <= itemStart + itemSize) {
|
|
176
|
+
containingIndex = index
|
|
177
|
+
}
|
|
178
|
+
const distance = Math.abs(mousePos - (itemStart + itemSize / 2))
|
|
179
|
+
if (distance < closestDistance) {
|
|
180
|
+
closestDistance = distance
|
|
181
|
+
closestIndex = index
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return containingIndex ?? closestIndex
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Set on the highlighted item (boolean attribute). */
|
|
189
|
+
export const ACTIVE_ATTR = 'data-fluid-hover-active'
|
|
190
|
+
/** Set on the container: the highlighted index, or absent. */
|
|
191
|
+
export const ACTIVE_INDEX_ATTR = 'data-fluid-hover-active-index'
|
|
192
|
+
|
|
193
|
+
const ACTIVATOR_SELECTOR =
|
|
194
|
+
"a[href], button, [role='menuitem'], [role='menuitemradio'], [role='menuitemcheckbox'], [role='option'], [role='radio'], [role='checkbox'], [role='tab'], [role='link'], [role='button']"
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The element a routed click should land on. A registered item is usually
|
|
198
|
+
* the interactive row itself; when it is only a box around one (a sidebar
|
|
199
|
+
* row around its button, a card around its link), the first control inside
|
|
200
|
+
* is what a real click on the row would have reached.
|
|
201
|
+
*/
|
|
202
|
+
function resolveActivator(element: HTMLElement): HTMLElement {
|
|
203
|
+
if (element.matches(ACTIVATOR_SELECTOR) || element.hasAttribute('tabindex')) {
|
|
204
|
+
return element
|
|
205
|
+
}
|
|
206
|
+
return element.querySelector<HTMLElement>(ACTIVATOR_SELECTOR) ?? element
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* How many frames the coalesced remeasure retries while the registered items
|
|
211
|
+
* still have no layout box. A popup can be in the DOM one frame before it is
|
|
212
|
+
* laid out; retrying beats publishing zeroed rects, and the cap keeps a list
|
|
213
|
+
* that stays hidden for good from spinning frames forever.
|
|
214
|
+
*/
|
|
215
|
+
const measurementAttempts = 3
|
|
216
|
+
|
|
217
|
+
export function useFluidHover<T extends HTMLElement>(
|
|
218
|
+
containerRef: RefObject<T | null>,
|
|
219
|
+
options: UseFluidHoverOptions = {},
|
|
220
|
+
): UseFluidHoverReturn {
|
|
221
|
+
const { axis = 'y', isItemDisabled, gapClick = true } = options
|
|
222
|
+
const gapClickMaxDistance = typeof gapClick === 'object' ? (gapClick.maxDistance ?? Infinity) : Infinity
|
|
223
|
+
const itemsRef = useRef(new Map<number, HTMLElement>())
|
|
224
|
+
const [activeIndex, setActiveIndex] = useState<number | null>(null)
|
|
225
|
+
// Mirrored for handlers that read it outside a render (the gap click).
|
|
226
|
+
const activeIndexRef = useRef<number | null>(null)
|
|
227
|
+
activeIndexRef.current = activeIndex
|
|
228
|
+
|
|
229
|
+
// The state, in the DOM: `data-fluid-hover-active` on the highlighted item
|
|
230
|
+
// and `data-fluid-hover-active-index` on the container. Devtools shows it
|
|
231
|
+
// and a test asserts on it without waiting for a frame. React does not
|
|
232
|
+
// manage these attributes, so it never clobbers them.
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
const container = containerRef.current
|
|
235
|
+
if (activeIndex === null) container?.removeAttribute(ACTIVE_INDEX_ATTR)
|
|
236
|
+
else container?.setAttribute(ACTIVE_INDEX_ATTR, String(activeIndex))
|
|
237
|
+
const active = activeIndex === null ? undefined : itemsRef.current.get(activeIndex)
|
|
238
|
+
active?.setAttribute(ACTIVE_ATTR, '')
|
|
239
|
+
return () => {
|
|
240
|
+
active?.removeAttribute(ACTIVE_ATTR)
|
|
241
|
+
// A row that re-registered under this index while it was highlighted
|
|
242
|
+
// (a remount under a new key) was marked by registerItem, not by this
|
|
243
|
+
// effect: drop the mark from whatever element holds the index now.
|
|
244
|
+
if (activeIndex !== null) itemsRef.current.get(activeIndex)?.removeAttribute(ACTIVE_ATTR)
|
|
245
|
+
}
|
|
246
|
+
}, [activeIndex, containerRef])
|
|
247
|
+
const [itemRects, setItemRects] = useState<ItemRect[]>([])
|
|
248
|
+
const [isMeasured, setIsMeasured] = useState(false)
|
|
249
|
+
const itemRectsRef = useRef<ItemRect[]>([])
|
|
250
|
+
const sessionRef = useRef(0)
|
|
251
|
+
const rafIdRef = useRef<number | null>(null)
|
|
252
|
+
const remeasureRafIdRef = useRef<number | null>(null)
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Publishes a rect for every registered item. Returns false when the
|
|
256
|
+
* measurement could not be completed (no container, or an item without a
|
|
257
|
+
* layout box) — nothing is published in that case, so the last complete
|
|
258
|
+
* measurement stands instead of being overwritten with zeroes.
|
|
259
|
+
*/
|
|
260
|
+
const runMeasurement = useCallback(() => {
|
|
261
|
+
const container = containerRef.current
|
|
262
|
+
if (!container) return false
|
|
263
|
+
const rects: ItemRect[] = []
|
|
264
|
+
let everyItemHasLayout = true
|
|
265
|
+
itemsRef.current.forEach((element, index) => {
|
|
266
|
+
// An element inside a display:none / not-yet-laid-out popup has no
|
|
267
|
+
// offsetParent and reports every offset as 0. Publishing that would pin
|
|
268
|
+
// overlays to the top of the list, so treat the whole pass as
|
|
269
|
+
// incomplete. A boxless element is the only case: `position: fixed`
|
|
270
|
+
// items also have no offsetParent but do have a size.
|
|
271
|
+
const hasLayoutBox = element.offsetParent !== null || element.offsetWidth > 0 || element.offsetHeight > 0
|
|
272
|
+
if (!hasLayoutBox) {
|
|
273
|
+
everyItemHasLayout = false
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
// Use offset* instead of getBoundingClientRect so measurements are
|
|
277
|
+
// unaffected by CSS transforms (e.g. scaleY animation on the parent
|
|
278
|
+
// motion.div). offsetTop/offsetLeft are layout values relative to the
|
|
279
|
+
// offsetParent (the scroll container), matching the coordinate space
|
|
280
|
+
// used by `position: absolute` children. Items nested inside positioned
|
|
281
|
+
// descendants of the container (a sidebar sub-menu's rows live inside a
|
|
282
|
+
// positioned row) accumulate those ancestors' offsets, so every rect
|
|
283
|
+
// lands in the container's own coordinate space; for a flat list the
|
|
284
|
+
// loop never runs and this is exactly the plain offsetTop/offsetLeft.
|
|
285
|
+
let top = element.offsetTop
|
|
286
|
+
let left = element.offsetLeft
|
|
287
|
+
let ancestor = element.offsetParent as HTMLElement | null
|
|
288
|
+
while (ancestor && ancestor !== container && container.contains(ancestor)) {
|
|
289
|
+
top += ancestor.offsetTop + ancestor.clientTop
|
|
290
|
+
left += ancestor.offsetLeft + ancestor.clientLeft
|
|
291
|
+
ancestor = ancestor.offsetParent as HTMLElement | null
|
|
292
|
+
}
|
|
293
|
+
rects[index] = {
|
|
294
|
+
top,
|
|
295
|
+
height: element.offsetHeight,
|
|
296
|
+
left,
|
|
297
|
+
width: element.offsetWidth,
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
if (!everyItemHasLayout) return false
|
|
301
|
+
// Skip the state update when nothing moved (a cheap top/left/width/height
|
|
302
|
+
// compare) so redundant remeasures don't churn re-renders.
|
|
303
|
+
const prev = itemRectsRef.current
|
|
304
|
+
let changed = prev.length !== rects.length
|
|
305
|
+
for (let i = 0; !changed && i < rects.length; i++) {
|
|
306
|
+
const p = prev[i]
|
|
307
|
+
const r = rects[i]
|
|
308
|
+
if (p === r) continue // both undefined (sparse slot)
|
|
309
|
+
changed = !p || !r || p.top !== r.top || p.left !== r.left || p.width !== r.width || p.height !== r.height
|
|
310
|
+
}
|
|
311
|
+
if (changed) {
|
|
312
|
+
itemRectsRef.current = rects
|
|
313
|
+
setItemRects(rects)
|
|
314
|
+
}
|
|
315
|
+
return true
|
|
316
|
+
}, [containerRef])
|
|
317
|
+
|
|
318
|
+
const measureItems = useCallback(() => {
|
|
319
|
+
runMeasurement()
|
|
320
|
+
}, [runMeasurement])
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* The hook's single measurement pass: coalesces every trigger (item
|
|
324
|
+
* registration, container resize) into one remeasure on the next frame and
|
|
325
|
+
* is the only place readiness is reported, so `isMeasured` can never turn
|
|
326
|
+
* true while another pass is still queued.
|
|
327
|
+
*/
|
|
328
|
+
const scheduleMeasurement = useCallback(
|
|
329
|
+
(attemptsLeft: number) => {
|
|
330
|
+
if (remeasureRafIdRef.current !== null) {
|
|
331
|
+
cancelAnimationFrame(remeasureRafIdRef.current)
|
|
332
|
+
}
|
|
333
|
+
remeasureRafIdRef.current = requestAnimationFrame(() => {
|
|
334
|
+
remeasureRafIdRef.current = null
|
|
335
|
+
if (runMeasurement()) {
|
|
336
|
+
setIsMeasured(true)
|
|
337
|
+
} else if (attemptsLeft > 1) {
|
|
338
|
+
scheduleMeasurement(attemptsLeft - 1)
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
},
|
|
342
|
+
[runMeasurement],
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
const remeasure = useCallback(() => {
|
|
346
|
+
// Readiness drops first: until the pass below settles, the published rects
|
|
347
|
+
// may not describe what is on screen, and an overlay positioned from them
|
|
348
|
+
// would be corrected after mounting — which animates as a slide.
|
|
349
|
+
setIsMeasured(false)
|
|
350
|
+
scheduleMeasurement(measurementAttempts)
|
|
351
|
+
}, [scheduleMeasurement])
|
|
352
|
+
|
|
353
|
+
// Observes the registered items themselves (not just the container): rows
|
|
354
|
+
// that change size in place — e.g. the site-wide size step flipping while a
|
|
355
|
+
// selection background is up — must invalidate the published rects even when
|
|
356
|
+
// the container the effect below captured has since been remounted and the
|
|
357
|
+
// ref points at a different element than the one being observed.
|
|
358
|
+
const itemRoRef = useRef<ResizeObserver | null>(null)
|
|
359
|
+
const getItemRo = useCallback(() => {
|
|
360
|
+
if (itemRoRef.current === null && typeof ResizeObserver !== 'undefined') {
|
|
361
|
+
itemRoRef.current = new ResizeObserver(() => scheduleMeasurement(measurementAttempts))
|
|
362
|
+
}
|
|
363
|
+
return itemRoRef.current
|
|
364
|
+
}, [scheduleMeasurement])
|
|
365
|
+
|
|
366
|
+
const registerItem = useCallback(
|
|
367
|
+
(index: number, element: HTMLElement | null) => {
|
|
368
|
+
if (element) {
|
|
369
|
+
itemsRef.current.set(index, element)
|
|
370
|
+
getItemRo()?.observe(element)
|
|
371
|
+
if (index === activeIndexRef.current) element.setAttribute(ACTIVE_ATTR, '')
|
|
372
|
+
} else {
|
|
373
|
+
const previous = itemsRef.current.get(index)
|
|
374
|
+
if (previous) itemRoRef.current?.unobserve(previous)
|
|
375
|
+
// The mark leaves with the element: a row that only moved to another
|
|
376
|
+
// index (a filtering list re-ordering) must not carry it there.
|
|
377
|
+
previous?.removeAttribute(ACTIVE_ATTR)
|
|
378
|
+
itemsRef.current.delete(index)
|
|
379
|
+
// The highlighted row is gone: nothing should stay lit or receive a
|
|
380
|
+
// routed click until the pointer picks again. Decided when the
|
|
381
|
+
// update applies, after this commit's registrations, so a row that
|
|
382
|
+
// only moved index hands the highlight to the row now under it.
|
|
383
|
+
if (index === activeIndexRef.current) {
|
|
384
|
+
setActiveIndex(current => (current === index && !itemsRef.current.has(index) ? null : current))
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// Coalesce rapid register/unregister calls (e.g. when an AnimatePresence
|
|
388
|
+
// remounts a list of rows) into a single remeasure on the next frame,
|
|
389
|
+
// so consumers don't have to manually call measureItems after the
|
|
390
|
+
// container's children swap.
|
|
391
|
+
remeasure()
|
|
392
|
+
},
|
|
393
|
+
[remeasure, getItemRo],
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
const handleMouseMove = useCallback(
|
|
397
|
+
(e: React.MouseEvent) => {
|
|
398
|
+
const mouseX = e.clientX
|
|
399
|
+
const mouseY = e.clientY
|
|
400
|
+
|
|
401
|
+
if (rafIdRef.current !== null) {
|
|
402
|
+
cancelAnimationFrame(rafIdRef.current)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
rafIdRef.current = requestAnimationFrame(() => {
|
|
406
|
+
rafIdRef.current = null
|
|
407
|
+
const container = containerRef.current
|
|
408
|
+
if (!container) return
|
|
409
|
+
setActiveIndex(
|
|
410
|
+
pickNearest({
|
|
411
|
+
axis,
|
|
412
|
+
point: { x: mouseX, y: mouseY },
|
|
413
|
+
rects: itemRectsRef.current,
|
|
414
|
+
containerRect: container.getBoundingClientRect(),
|
|
415
|
+
scroll: { x: container.scrollLeft, y: container.scrollTop },
|
|
416
|
+
border: { x: container.clientLeft, y: container.clientTop },
|
|
417
|
+
layoutSize: { width: container.offsetWidth, height: container.offsetHeight },
|
|
418
|
+
isDisabled: isItemDisabled
|
|
419
|
+
? index => {
|
|
420
|
+
const el = itemsRef.current.get(index)
|
|
421
|
+
return !!el && isItemDisabled(el)
|
|
422
|
+
}
|
|
423
|
+
: undefined,
|
|
424
|
+
}),
|
|
425
|
+
)
|
|
426
|
+
})
|
|
427
|
+
},
|
|
428
|
+
[axis, containerRef, isItemDisabled],
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
const handleMouseEnter = useCallback(() => {
|
|
432
|
+
sessionRef.current += 1
|
|
433
|
+
}, [])
|
|
434
|
+
|
|
435
|
+
const handleMouseLeave = useCallback(() => {
|
|
436
|
+
if (rafIdRef.current !== null) {
|
|
437
|
+
cancelAnimationFrame(rafIdRef.current)
|
|
438
|
+
rafIdRef.current = null
|
|
439
|
+
}
|
|
440
|
+
setActiveIndex(null)
|
|
441
|
+
}, [])
|
|
442
|
+
|
|
443
|
+
const handleClick = useCallback(
|
|
444
|
+
(e: React.MouseEvent) => {
|
|
445
|
+
const target = e.target as Node | null
|
|
446
|
+
if (!target) return
|
|
447
|
+
// Inside an item: the item owns the click.
|
|
448
|
+
for (const element of itemsRef.current.values()) {
|
|
449
|
+
if (element.contains(target)) return
|
|
450
|
+
}
|
|
451
|
+
// A row that unmounted while its own click was still bubbling (a pick
|
|
452
|
+
// whose primitive re-renders the list synchronously, like a "create"
|
|
453
|
+
// row that becomes a real item) already landed; it is not a gap.
|
|
454
|
+
if (!target.isConnected) return
|
|
455
|
+
// A control that sits between the rows (a search field at the top of
|
|
456
|
+
// a menu, a footer button) keeps its own click too.
|
|
457
|
+
const control = (target as Element).closest?.(
|
|
458
|
+
"input, textarea, select, button, a, summary, [contenteditable], [role='textbox'], [role='searchbox'], [role='button']",
|
|
459
|
+
)
|
|
460
|
+
if (control) return
|
|
461
|
+
if (gapClick === false) return
|
|
462
|
+
const index = activeIndexRef.current
|
|
463
|
+
if (index === null) return
|
|
464
|
+
const element = itemsRef.current.get(index)
|
|
465
|
+
if (!element || isItemDisabled?.(element)) return
|
|
466
|
+
if (gapClickMaxDistance !== Infinity) {
|
|
467
|
+
const r = element.getBoundingClientRect()
|
|
468
|
+
const dx = Math.max(r.left - e.clientX, 0, e.clientX - r.right)
|
|
469
|
+
const dy = Math.max(r.top - e.clientY, 0, e.clientY - r.bottom)
|
|
470
|
+
if (Math.hypot(dx, dy) > gapClickMaxDistance) return
|
|
471
|
+
}
|
|
472
|
+
// A real DOM click on the item, so its own handlers (and the primitive
|
|
473
|
+
// wrapping it, if any) run exactly as if the pointer had been inside.
|
|
474
|
+
resolveActivator(element).click()
|
|
475
|
+
},
|
|
476
|
+
[isItemDisabled, gapClick, gapClickMaxDistance],
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
// Remeasure when the container resizes — a reflow moves items even though
|
|
480
|
+
// the registered set is unchanged, which would otherwise leave itemRects
|
|
481
|
+
// stale. Coalesced through the same rAF as register/unregister. Readiness is
|
|
482
|
+
// deliberately not dropped: the item set is unchanged, so the published rects
|
|
483
|
+
// stay usable, and hiding overlays on every reflow would flicker them.
|
|
484
|
+
useEffect(() => {
|
|
485
|
+
const container = containerRef.current
|
|
486
|
+
if (!container || typeof ResizeObserver === 'undefined') return
|
|
487
|
+
const ro = new ResizeObserver(() => scheduleMeasurement(measurementAttempts))
|
|
488
|
+
ro.observe(container)
|
|
489
|
+
return () => ro.disconnect()
|
|
490
|
+
}, [containerRef, scheduleMeasurement])
|
|
491
|
+
|
|
492
|
+
// Clean up rAF and the item observer on unmount
|
|
493
|
+
useEffect(() => {
|
|
494
|
+
return () => {
|
|
495
|
+
if (rafIdRef.current !== null) {
|
|
496
|
+
cancelAnimationFrame(rafIdRef.current)
|
|
497
|
+
}
|
|
498
|
+
if (remeasureRafIdRef.current !== null) {
|
|
499
|
+
cancelAnimationFrame(remeasureRafIdRef.current)
|
|
500
|
+
}
|
|
501
|
+
itemRoRef.current?.disconnect()
|
|
502
|
+
itemRoRef.current = null
|
|
503
|
+
}
|
|
504
|
+
}, [])
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
activeIndex,
|
|
508
|
+
setActiveIndex,
|
|
509
|
+
itemRects,
|
|
510
|
+
isMeasured,
|
|
511
|
+
sessionRef,
|
|
512
|
+
handlers: {
|
|
513
|
+
onMouseMove: handleMouseMove,
|
|
514
|
+
onMouseEnter: handleMouseEnter,
|
|
515
|
+
onMouseLeave: handleMouseLeave,
|
|
516
|
+
onClick: handleClick,
|
|
517
|
+
},
|
|
518
|
+
registerItem,
|
|
519
|
+
remeasure,
|
|
520
|
+
measureItems,
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Registers an item's element with its list for as long as it is mounted.
|
|
526
|
+
* The one way rows join a list: pass the hook's `registerItem` (or the copy
|
|
527
|
+
* a context hands down), the row's index, and its ref. Either may be
|
|
528
|
+
* missing for a row rendered outside a list (a standalone card, an accordion
|
|
529
|
+
* item that is not grouped); then nothing is registered.
|
|
530
|
+
*/
|
|
531
|
+
export function useRegisterFluidHoverItem(
|
|
532
|
+
registerItem: ((index: number, element: HTMLElement | null) => void) | undefined,
|
|
533
|
+
index: number | undefined,
|
|
534
|
+
ref: RefObject<HTMLElement | null>,
|
|
535
|
+
) {
|
|
536
|
+
useEffect(() => {
|
|
537
|
+
if (!registerItem || index === undefined) return
|
|
538
|
+
registerItem(index, ref.current)
|
|
539
|
+
return () => registerItem(index, null)
|
|
540
|
+
}, [index, registerItem, ref])
|
|
541
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Vendored from Fluid Functionalism — `registry/default/sidebar-core.tsx` at
|
|
3
|
+
* github.com/mickadesign/fluid-functionalism@b3587bdbd83fc66c2a6aae3817ffb856cb09260b
|
|
4
|
+
* (fluidfunctionalism.com). MIT License © 2026 Micka Touillaud — see
|
|
5
|
+
* LICENSE.fluid-functionalism in this package.
|
|
6
|
+
*
|
|
7
|
+
* Local modifications:
|
|
8
|
+
* - Moved out of the sidebar so the date and time pickers share it; exported.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useEffect, useState } from 'react'
|
|
12
|
+
|
|
13
|
+
// Starts undefined so the server and first client render agree (both treat it
|
|
14
|
+
// as desktop); the media query corrects it in an effect before interaction.
|
|
15
|
+
export function useIsMobile(breakpoint: number): boolean {
|
|
16
|
+
const [isMobile, setIsMobile] = useState<boolean | undefined>(undefined)
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const mql = window.matchMedia(`(max-width: ${breakpoint - 1}px)`)
|
|
19
|
+
const onChange = () => setIsMobile(mql.matches)
|
|
20
|
+
onChange()
|
|
21
|
+
mql.addEventListener('change', onChange)
|
|
22
|
+
return () => mql.removeEventListener('change', onChange)
|
|
23
|
+
}, [breakpoint])
|
|
24
|
+
return !!isMobile
|
|
25
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Vendored from Fluid Functionalism — `registry/default/hooks/use-keyboard-nav-gate.ts` at
|
|
3
|
+
* github.com/mickadesign/fluid-functionalism@b3587bdbd83fc66c2a6aae3817ffb856cb09260b
|
|
4
|
+
* (fluidfunctionalism.com). MIT License © 2026 Micka Touillaud — see
|
|
5
|
+
* LICENSE.fluid-functionalism in this package.
|
|
6
|
+
*
|
|
7
|
+
* Local modifications:
|
|
8
|
+
* - `"use client"` directive dropped (no RSC here).
|
|
9
|
+
* - `@/lib/*` and `@/hooks/*` imports rewritten to sibling `./*` paths.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { type KeyboardEvent, useCallback, useEffect, useRef } from 'react'
|
|
13
|
+
import { POPUP_NAV_KEYS } from './popup'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Gates a popup's keyboard focus ring on keyboard use.
|
|
17
|
+
*
|
|
18
|
+
* Rows take focus as a popup opens (the primitive's own autofocus, or the
|
|
19
|
+
* popup's first-row focus), and Chrome grants `:focus-visible` to any
|
|
20
|
+
* script-driven focus — so without a gate every pointer-opened popup would
|
|
21
|
+
* open with a ring on its first row. The flag is seeded at open from the
|
|
22
|
+
* element that has focus then: a trigger reached by keyboard matches
|
|
23
|
+
* `:focus-visible`, a clicked one doesn't. Navigation keys inside the popup
|
|
24
|
+
* earn the ring afterwards. Attach `trackKeyboardNav` in the CAPTURE phase —
|
|
25
|
+
* the primitive moves focus during its own keydown handling, so the flag
|
|
26
|
+
* must be set before then.
|
|
27
|
+
*/
|
|
28
|
+
export function useKeyboardNavGate(open: boolean) {
|
|
29
|
+
const keyboardNavRef = useRef(false)
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!open) return
|
|
33
|
+
// Script focus inherits :focus-visible from the previously focused
|
|
34
|
+
// element, so this reads the same whether the trigger still has focus
|
|
35
|
+
// or the primitive has already moved it into the popup.
|
|
36
|
+
const active = document.activeElement
|
|
37
|
+
keyboardNavRef.current = active instanceof HTMLElement && active.matches(':focus-visible')
|
|
38
|
+
}, [open])
|
|
39
|
+
|
|
40
|
+
const trackKeyboardNav = useCallback((e: KeyboardEvent) => {
|
|
41
|
+
if (POPUP_NAV_KEYS.includes(e.key)) keyboardNavRef.current = true
|
|
42
|
+
}, [])
|
|
43
|
+
|
|
44
|
+
return { keyboardNavRef, trackKeyboardNav }
|
|
45
|
+
}
|