@justanarthur/modo-ui 0.6.0-canary.8 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -164
- package/dist/base.css +4 -141
- package/dist/blocks/sidebar-app/index.js +26 -40
- package/dist/components/AI/input-message/index.js +16 -23
- package/dist/components/AI/thinking-indicator/index.js +6 -9
- package/dist/components/accordion/index.d.ts +11 -39
- package/dist/components/accordion/index.js +13 -18
- package/dist/components/badge/index.d.ts +11 -18
- package/dist/components/badge/index.js +7 -12
- package/dist/components/button/index.d.ts +17 -53
- package/dist/components/button/index.js +6 -11
- package/dist/components/card/index.d.ts +9 -32
- package/dist/components/card/index.js +11 -17
- package/dist/components/copy/index.d.ts +8 -41
- package/dist/components/copy/index.js +9 -15
- package/dist/components/inputs/checkbox/index.d.ts +9 -28
- package/dist/components/inputs/checkbox/index.js +13 -18
- package/dist/components/inputs/color-picker/index.d.ts +14 -22
- package/dist/components/inputs/color-picker/index.js +18 -26
- package/dist/components/inputs/combobox/index.d.ts +20 -85
- package/dist/components/inputs/combobox/index.js +18 -27
- package/dist/components/inputs/date-time/calendar/index.d.ts +18 -24
- package/dist/components/inputs/date-time/calendar/index.js +11 -15
- package/dist/components/inputs/date-time/date-picker/index.d.ts +16 -105
- package/dist/components/inputs/date-time/date-picker/index.js +25 -35
- package/dist/components/inputs/date-time/date-range-picker/index.d.ts +12 -106
- package/dist/components/inputs/date-time/date-range-picker/index.js +24 -35
- package/dist/components/inputs/date-time/duration-picker/index.d.ts +13 -66
- package/dist/components/inputs/date-time/duration-picker/index.js +24 -33
- package/dist/components/inputs/date-time/month-picker/index.d.ts +10 -60
- package/dist/components/inputs/date-time/month-picker/index.js +23 -32
- package/dist/components/inputs/date-time/time-picker/index.d.ts +10 -58
- package/dist/components/inputs/date-time/time-picker/index.js +24 -33
- package/dist/components/inputs/input/index.d.ts +13 -95
- package/dist/components/inputs/input/index.js +9 -18
- package/dist/components/inputs/radio/index.d.ts +5 -20
- package/dist/components/inputs/radio/index.js +12 -16
- package/dist/components/inputs/search/index.d.ts +3 -3
- package/dist/components/inputs/search/index.js +18 -26
- package/dist/components/inputs/select/index.d.ts +23 -99
- package/dist/components/inputs/select/index.js +14 -29
- package/dist/components/inputs/slider/index.d.ts +0 -9
- package/dist/components/inputs/slider/index.js +5 -8
- package/dist/components/inputs/switch/index.d.ts +2 -8
- package/dist/components/inputs/switch/index.js +12 -15
- package/dist/components/inputs/textarea/index.d.ts +1 -20
- package/dist/components/inputs/textarea/index.js +6 -9
- package/dist/components/navigation/sidebar/index.d.ts +21 -44
- package/dist/components/navigation/sidebar/index.js +31 -42
- package/dist/components/navigation/tabs/index.d.ts +2 -8
- package/dist/components/navigation/tabs/index.js +10 -15
- package/dist/components/navigation/tabs-subtle/index.js +10 -13
- package/dist/components/overlays/alert-dialog/index.d.ts +16 -135
- package/dist/components/overlays/alert-dialog/index.js +13 -23
- package/dist/components/overlays/command/index.d.ts +20 -25
- package/dist/components/overlays/command/index.js +17 -25
- package/dist/components/overlays/context-menu/index.d.ts +17 -39
- package/dist/components/overlays/context-menu/index.js +18 -29
- package/dist/components/overlays/dialog/index.d.ts +16 -42
- package/dist/components/overlays/dialog/index.js +12 -20
- package/dist/components/overlays/dropdown/index.d.ts +34 -304
- package/dist/components/overlays/dropdown/index.js +17 -44
- package/dist/components/overlays/popover/index.d.ts +12 -38
- package/dist/components/overlays/popover/index.js +9 -13
- package/dist/components/overlays/preview-card/index.js +8 -12
- package/dist/components/overlays/sheet/index.d.ts +8 -25
- package/dist/components/overlays/sheet/index.js +13 -19
- package/dist/components/overlays/toast/index.d.ts +50 -209
- package/dist/components/overlays/toast/index.js +10 -23
- package/dist/components/overlays/tooltip/index.d.ts +21 -123
- package/dist/components/overlays/tooltip/index.js +6 -16
- package/dist/components/table/index.d.ts +8 -63
- package/dist/components/table/index.js +9 -20
- package/dist/index.d.ts +1048 -2999
- package/dist/index.js +82 -231
- package/dist/preset.d.ts +0 -22
- package/dist/preset.js +2 -24
- package/dist/primitives/code/index.js +5 -8
- package/dist/primitives/craft/index.js +6 -10
- package/dist/primitives/fluid-hover/index.js +8 -10
- package/dist/primitives/morph/index.js +10 -16
- package/dist/primitives/motion/index.js +8 -11
- package/dist/primitives/scroll-area/index.js +3 -5
- package/dist/primitives/sizes/index.js +4 -6
- package/dist/primitives/surface/index.js +4 -6
- package/dist/shared/{components-4y816hwp.js → components-0q0sqfh4.js} +49 -27
- package/dist/shared/components-0sn7ypjw.js +61 -0
- package/dist/shared/{components-vc864c51.js → components-11da9ajg.js} +5 -8
- package/dist/shared/components-1434p1mh.js +123 -0
- package/dist/shared/{components-t2a3qmy9.js → components-1z6tdadk.js} +24 -40
- package/dist/shared/{components-qs4rkqsj.js → components-27nnvrwe.js} +14 -15
- package/dist/shared/{components-av5t26rc.js → components-2dxkv6zm.js} +7 -8
- package/dist/shared/{components-wqrvqq5p.js → components-2ecg8svx.js} +152 -234
- package/dist/shared/{components-3ydr2fqe.js → components-2ph6t61s.js} +34 -37
- package/dist/shared/{components-6gk118w3.js → components-39jq9va1.js} +24 -29
- package/dist/shared/components-3ky44908.js +9 -0
- package/dist/shared/{components-y1qvbawa.js → components-3mkepka5.js} +25 -28
- package/dist/shared/{components-xqszvkj4.js → components-4700p9kz.js} +22 -24
- package/dist/shared/{components-yhgq0qxx.js → components-59efxjd9.js} +12 -13
- package/dist/shared/{components-9j9abkct.js → components-5vgpnxgy.js} +27 -51
- package/dist/shared/{components-8tt3xbb5.js → components-5xc2j8jz.js} +18 -19
- package/dist/shared/{components-jymmaevv.js → components-6p8tfcd6.js} +29 -57
- package/dist/shared/{components-4nck8acq.js → components-7r3etddy.js} +54 -19
- package/dist/shared/{components-skfd7kff.js → components-8snj490h.js} +32 -78
- package/dist/shared/{components-ctqmge8t.js → components-b0eac83j.js} +30 -9
- package/dist/shared/{components-9sttq78t.js → components-b2wyq2cq.js} +59 -41
- package/dist/shared/{components-vkwq2k96.js → components-b3c5et5b.js} +38 -49
- package/dist/shared/{components-rrbpmfx4.js → components-c6s6b43g.js} +2 -3
- package/dist/shared/{components-pw06rbxq.js → components-chacnk61.js} +19 -19
- package/dist/shared/{components-71nbt03m.js → components-cv16mryv.js} +7 -26
- package/dist/shared/{components-eqze3y30.js → components-d6xtjht3.js} +30 -35
- package/dist/shared/{components-5y5shqfd.js → components-dk0gjr5a.js} +18 -19
- package/dist/shared/{components-khspp26b.js → components-fzrq6tr3.js} +34 -37
- package/dist/shared/{components-km7fhgbk.js → components-gc5vtq2f.js} +30 -66
- package/dist/shared/components-gf5s6ap9.js +42 -0
- package/dist/shared/{components-qqcmtrbv.js → components-gz8zf3ec.js} +55 -56
- package/dist/shared/{components-mattt5jp.js → components-h2ajmhwn.js} +35 -107
- package/dist/shared/{components-awra5sff.js → components-hqeq3w9q.js} +16 -24
- package/dist/shared/{components-5tzhqbcw.js → components-hw9yk32z.js} +24 -36
- package/dist/shared/components-hyq23phv.js +310 -0
- package/dist/shared/{components-8mt5bsv6.js → components-jbbmdr74.js} +22 -41
- package/dist/shared/{components-dcc02hs2.js → components-jjw69c4c.js} +4 -7
- package/dist/shared/{components-2syq4mwc.js → components-jthe1pwb.js} +420 -139
- package/dist/shared/{components-r7q1zm7r.js → components-jwst3vtj.js} +76 -224
- package/dist/shared/{components-qdm3yh26.js → components-kvr7f9hy.js} +10 -19
- package/dist/shared/{components-s8yqk2dc.js → components-kwa6mrf9.js} +11 -46
- package/dist/shared/{components-de99ykt7.js → components-men63rwj.js} +2 -3
- package/dist/shared/{components-6b0zb8z3.js → components-mjthm1kf.js} +9 -13
- package/dist/shared/{components-2c64r8c9.js → components-n42c79bz.js} +22 -23
- package/dist/shared/{components-tz1fv35x.js → components-n7mjr357.js} +2 -3
- package/dist/shared/components-ng72k97e.js +128 -0
- package/dist/shared/{components-dyae0y7h.js → components-pbmqctfa.js} +65 -169
- package/dist/shared/{components-px2zm7dc.js → components-q5xj5c16.js} +8 -9
- package/dist/shared/components-raknhppb.js +64 -0
- package/dist/shared/{components-8rb25gbe.js → components-rr7a6ja9.js} +27 -59
- package/dist/shared/{components-5012r6gr.js → components-s083f38z.js} +34 -164
- package/dist/shared/components-s5ngg32c.js +267 -0
- package/dist/shared/components-sn1rxx1d.js +143 -0
- package/dist/shared/{components-r8eqs7gz.js → components-ta7shw28.js} +4 -4
- package/dist/shared/{components-9as3rf68.js → components-vc7vq5nz.js} +3 -6
- package/dist/shared/{components-ft3zdc79.js → components-vfbhxf5r.js} +5 -6
- package/dist/shared/components-vynjtrk6.js +19 -0
- package/dist/shared/{components-w0xq6q9k.js → components-w1r67ag9.js} +149 -294
- package/dist/shared/{components-z0bqtvcp.js → components-w90endve.js} +72 -71
- package/dist/shared/{components-t5pk5nmc.js → components-wvp1m52g.js} +102 -149
- package/dist/shared/components-wy9jpqny.js +146 -0
- package/dist/shared/{components-5sxjeh6h.js → components-y9y0g8er.js} +28 -52
- package/dist/shared/components-ypp01w3p.js +986 -0
- package/dist/shared/{components-t50hrswb.js → components-zfnmn5d4.js} +36 -82
- package/dist/shared/{components-jqpefjsn.js → components-zrkzcayz.js} +6 -54
- package/dist/shared/{components-vf45j12c.js → components-zx6b6yby.js} +18 -4
- package/dist/styles.css +32 -320
- package/dist/tailwind.css +0 -16
- package/dist/tokens.css +0 -67
- package/package.json +4 -227
- package/DESIGN.md +0 -623
- package/blocks/sidebar-app/examples.mdx +0 -33
- package/blocks/sidebar-app/index.tsx +0 -218
- package/blocks/sidebar-app/inset-topbar.tsx +0 -25
- package/blocks/sidebar-app/menu-grid.ts +0 -26
- package/blocks/sidebar-app/nav-data.ts +0 -36
- package/blocks/sidebar-app/search-field.tsx +0 -48
- package/blocks/sidebar-app/user-footer.tsx +0 -67
- package/blocks/sidebar-app/workspace-header.tsx +0 -129
- package/components/AI/input-message/examples.mdx +0 -130
- package/components/AI/input-message/file-thumbnail.tsx +0 -173
- package/components/AI/input-message/index.tsx +0 -1385
- package/components/AI/thinking-indicator/examples.mdx +0 -27
- package/components/AI/thinking-indicator/index.tsx +0 -210
- package/components/accordion/examples.mdx +0 -233
- package/components/accordion/index.tsx +0 -1071
- package/components/avatar/examples.mdx +0 -95
- package/components/avatar/index.tsx +0 -129
- package/components/badge/change-demo.tsx +0 -29
- package/components/badge/examples.mdx +0 -124
- package/components/badge/index.tsx +0 -225
- package/components/badge/variants.ts +0 -73
- package/components/button/examples.mdx +0 -168
- package/components/button/index.tsx +0 -424
- package/components/button/loading-demo.tsx +0 -25
- package/components/button/variants.ts +0 -168
- package/components/card/examples.mdx +0 -251
- package/components/card/index.tsx +0 -1070
- package/components/copy/examples.mdx +0 -125
- package/components/copy/index.tsx +0 -662
- package/components/inputs/checkbox/examples.mdx +0 -87
- package/components/inputs/checkbox/index.tsx +0 -611
- package/components/inputs/checkbox/select-all-demo.tsx +0 -48
- package/components/inputs/color-picker/examples.mdx +0 -111
- package/components/inputs/color-picker/index.tsx +0 -1981
- package/components/inputs/color-picker/variants.ts +0 -401
- package/components/inputs/combobox/examples.mdx +0 -366
- package/components/inputs/combobox/index.tsx +0 -1527
- package/components/inputs/combobox/remote-demo.tsx +0 -105
- package/components/inputs/date-time/calendar/examples.mdx +0 -45
- package/components/inputs/date-time/calendar/index.tsx +0 -370
- package/components/inputs/date-time/calendar/use-month-window.ts +0 -173
- package/components/inputs/date-time/date-picker/examples.mdx +0 -243
- package/components/inputs/date-time/date-picker/index.tsx +0 -476
- package/components/inputs/date-time/date-picker/masked-date-input.tsx +0 -348
- package/components/inputs/date-time/date-range-picker/examples.mdx +0 -93
- package/components/inputs/date-time/date-range-picker/index.tsx +0 -525
- package/components/inputs/date-time/duration-picker/examples.mdx +0 -40
- package/components/inputs/date-time/duration-picker/index.tsx +0 -253
- package/components/inputs/date-time/month-picker/examples.mdx +0 -45
- package/components/inputs/date-time/month-picker/index.tsx +0 -254
- package/components/inputs/date-time/picker-shell.tsx +0 -581
- package/components/inputs/date-time/time-columns.tsx +0 -94
- package/components/inputs/date-time/time-picker/examples.mdx +0 -57
- package/components/inputs/date-time/time-picker/index.tsx +0 -227
- package/components/inputs/field/examples.mdx +0 -117
- package/components/inputs/field/index.tsx +0 -268
- package/components/inputs/file-dropzone/examples.mdx +0 -83
- package/components/inputs/file-dropzone/file-row.tsx +0 -201
- package/components/inputs/file-dropzone/index.tsx +0 -316
- package/components/inputs/file-dropzone/upload-demo.tsx +0 -67
- package/components/inputs/file-dropzone/variants.ts +0 -94
- package/components/inputs/input/examples.mdx +0 -166
- package/components/inputs/input/index.tsx +0 -494
- package/components/inputs/input/password-demo.tsx +0 -30
- package/components/inputs/label/examples.mdx +0 -67
- package/components/inputs/label/index.tsx +0 -85
- package/components/inputs/number-input/examples.mdx +0 -69
- package/components/inputs/number-input/index.tsx +0 -221
- package/components/inputs/radio/examples.mdx +0 -77
- package/components/inputs/radio/index.tsx +0 -636
- package/components/inputs/search/examples.mdx +0 -74
- package/components/inputs/search/index.tsx +0 -333
- package/components/inputs/select/examples.mdx +0 -296
- package/components/inputs/select/index.tsx +0 -1044
- package/components/inputs/select/variants.ts +0 -32
- package/components/inputs/slider/examples.mdx +0 -153
- package/components/inputs/slider/index.tsx +0 -1867
- package/components/inputs/switch/examples.mdx +0 -96
- package/components/inputs/switch/index.tsx +0 -504
- package/components/inputs/textarea/examples.mdx +0 -126
- package/components/inputs/textarea/index.tsx +0 -207
- package/components/navigation/navigation-menu/examples.mdx +0 -136
- package/components/navigation/navigation-menu/index.tsx +0 -704
- package/components/navigation/pagination/examples.mdx +0 -74
- package/components/navigation/pagination/index.tsx +0 -401
- package/components/navigation/sidebar/examples.mdx +0 -325
- package/components/navigation/sidebar/index.tsx +0 -453
- package/components/navigation/sidebar/sidebar-core.tsx +0 -1408
- package/components/navigation/sidebar/sidebar-menu.tsx +0 -1495
- package/components/navigation/sidebar/sidebar.css +0 -74
- package/components/navigation/sidebar/variants.ts +0 -55
- package/components/navigation/tabs/examples.mdx +0 -89
- package/components/navigation/tabs/index.tsx +0 -494
- package/components/navigation/tabs-subtle/examples.mdx +0 -49
- package/components/navigation/tabs-subtle/index.tsx +0 -512
- package/components/overlays/alert-dialog/examples.mdx +0 -123
- package/components/overlays/alert-dialog/index.tsx +0 -276
- package/components/overlays/alert-dialog/pending-demo.tsx +0 -36
- package/components/overlays/command/examples.mdx +0 -113
- package/components/overlays/command/index.tsx +0 -425
- package/components/overlays/context-menu/examples.mdx +0 -56
- package/components/overlays/context-menu/index.tsx +0 -279
- package/components/overlays/dialog/examples.mdx +0 -165
- package/components/overlays/dialog/index.tsx +0 -451
- package/components/overlays/dropdown/dropdown-search.tsx +0 -395
- package/components/overlays/dropdown/examples.mdx +0 -444
- package/components/overlays/dropdown/index.tsx +0 -1241
- package/components/overlays/dropdown/menu-item.tsx +0 -510
- package/components/overlays/dropdown/menu-parts.tsx +0 -725
- package/components/overlays/popover/anchored-demo.tsx +0 -59
- package/components/overlays/popover/examples.mdx +0 -203
- package/components/overlays/popover/index.tsx +0 -317
- package/components/overlays/preview-card/examples.mdx +0 -61
- package/components/overlays/preview-card/index.tsx +0 -204
- package/components/overlays/sheet/examples.mdx +0 -101
- package/components/overlays/sheet/index.tsx +0 -307
- package/components/overlays/toast/examples.mdx +0 -85
- package/components/overlays/toast/global-demo.tsx +0 -72
- package/components/overlays/toast/index.tsx +0 -544
- package/components/overlays/toast/manager.ts +0 -193
- package/components/overlays/tooltip/examples.mdx +0 -106
- package/components/overlays/tooltip/index.tsx +0 -539
- package/components/progress/examples.mdx +0 -59
- package/components/progress/index.tsx +0 -174
- package/components/progress/progress.css +0 -49
- package/components/progress/step-demo.tsx +0 -27
- package/components/table/examples.mdx +0 -188
- package/components/table/index.tsx +0 -535
- package/components/table/sort-demo.tsx +0 -61
- package/dist/components/avatar/index.d.ts +0 -46
- package/dist/components/avatar/index.js +0 -19
- package/dist/components/badge/variants.d.ts +0 -33
- package/dist/components/badge/variants.js +0 -10
- package/dist/components/button/variants.d.ts +0 -39
- package/dist/components/button/variants.js +0 -8
- package/dist/components/inputs/color-picker/variants.d.ts +0 -61
- package/dist/components/inputs/color-picker/variants.js +0 -28
- package/dist/components/inputs/field/index.d.ts +0 -116
- package/dist/components/inputs/field/index.js +0 -29
- package/dist/components/inputs/file-dropzone/index.d.ts +0 -167
- package/dist/components/inputs/file-dropzone/index.js +0 -33
- package/dist/components/inputs/file-dropzone/variants.d.ts +0 -35
- package/dist/components/inputs/file-dropzone/variants.js +0 -12
- package/dist/components/inputs/label/index.d.ts +0 -32
- package/dist/components/inputs/label/index.js +0 -11
- package/dist/components/inputs/number-input/index.d.ts +0 -87
- package/dist/components/inputs/number-input/index.js +0 -21
- package/dist/components/inputs/select/variants.d.ts +0 -5
- package/dist/components/inputs/select/variants.js +0 -6
- package/dist/components/navigation/navigation-menu/index.d.ts +0 -126
- package/dist/components/navigation/navigation-menu/index.js +0 -41
- package/dist/components/navigation/pagination/index.d.ts +0 -75
- package/dist/components/navigation/pagination/index.js +0 -41
- package/dist/components/navigation/sidebar/variants.d.ts +0 -25
- package/dist/components/navigation/sidebar/variants.js +0 -28
- package/dist/components/progress/index.d.ts +0 -56
- package/dist/components/progress/index.js +0 -15
- package/dist/docs/index.d.ts +0 -83
- package/dist/docs/index.js +0 -288
- package/dist/docs/vite.d.ts +0 -31
- package/dist/docs/vite.js +0 -45
- package/dist/lib/date-locale.d.ts +0 -26
- package/dist/lib/date-locale.js +0 -24
- package/dist/lib/field-classes.d.ts +0 -149
- package/dist/lib/field-classes.js +0 -41
- package/dist/lib/field-variants.d.ts +0 -47
- package/dist/lib/field-variants.js +0 -26
- package/dist/lib/fluid-hover-highlight.d.ts +0 -112
- package/dist/lib/fluid-hover-highlight.js +0 -17
- package/dist/lib/focus-ring.d.ts +0 -18
- package/dist/lib/focus-ring.js +0 -11
- package/dist/lib/goo-indicator.d.ts +0 -46
- package/dist/lib/goo-indicator.js +0 -14
- package/dist/lib/icon-context.d.ts +0 -30
- package/dist/lib/icon-context.js +0 -12
- package/dist/lib/inert.d.ts +0 -9
- package/dist/lib/inert.js +0 -8
- package/dist/lib/morph-layers.d.ts +0 -93
- package/dist/lib/morph-layers.js +0 -13
- package/dist/lib/morph-part.d.ts +0 -21
- package/dist/lib/morph-part.js +0 -12
- package/dist/lib/popup.d.ts +0 -20
- package/dist/lib/popup.js +0 -12
- package/dist/lib/reduced-motion.d.ts +0 -9
- package/dist/lib/reduced-motion.js +0 -6
- package/dist/lib/row-index.d.ts +0 -10
- package/dist/lib/row-index.js +0 -8
- package/dist/lib/row-order.d.ts +0 -28
- package/dist/lib/row-order.js +0 -10
- package/dist/lib/scroll-column.d.ts +0 -31
- package/dist/lib/scroll-column.js +0 -19
- package/dist/lib/shape-context.d.ts +0 -30
- package/dist/lib/shape-context.js +0 -13
- package/dist/lib/shape-radius.d.ts +0 -19
- package/dist/lib/shape-radius.js +0 -6
- package/dist/lib/size-context.d.ts +0 -94
- package/dist/lib/size-context.js +0 -20
- package/dist/lib/size-scale.d.ts +0 -79
- package/dist/lib/size-scale.js +0 -8
- package/dist/lib/slot.d.ts +0 -39
- package/dist/lib/slot.js +0 -14
- package/dist/lib/springs.d.ts +0 -44
- package/dist/lib/springs.js +0 -8
- package/dist/lib/surface-classes.d.ts +0 -19
- package/dist/lib/surface-classes.js +0 -10
- package/dist/lib/surface-context.d.ts +0 -9
- package/dist/lib/surface-context.js +0 -8
- package/dist/lib/use-controllable-state.d.ts +0 -2
- package/dist/lib/use-controllable-state.js +0 -6
- package/dist/lib/use-fluid-hover.d.ts +0 -138
- package/dist/lib/use-fluid-hover.js +0 -14
- package/dist/lib/use-is-mobile.d.ts +0 -2
- package/dist/lib/use-is-mobile.js +0 -6
- package/dist/lib/use-keyboard-nav-gate.d.ts +0 -20
- package/dist/lib/use-keyboard-nav-gate.js +0 -7
- package/dist/lib/use-merge-split.d.ts +0 -47
- package/dist/lib/use-merge-split.js +0 -15
- package/dist/lib/use-morph.d.ts +0 -101
- package/dist/lib/use-morph.js +0 -20
- package/dist/lib/use-touch-primary.d.ts +0 -2
- package/dist/lib/use-touch-primary.js +0 -6
- package/dist/lib/utils.d.ts +0 -3
- package/dist/lib/utils.js +0 -6
- package/dist/primitives/separator/index.d.ts +0 -25
- package/dist/primitives/separator/index.js +0 -9
- package/dist/primitives/skeleton/index.d.ts +0 -35
- package/dist/primitives/skeleton/index.js +0 -13
- package/dist/shared/components-124epb7f.js +0 -53
- package/dist/shared/components-1kab3j44.js +0 -337
- package/dist/shared/components-26snnbs9.js +0 -322
- package/dist/shared/components-34txk9rj.js +0 -35
- package/dist/shared/components-3h92d94m.js +0 -23
- package/dist/shared/components-4vfm04wq.js +0 -82
- package/dist/shared/components-54nq988h.js +0 -123
- package/dist/shared/components-7s7pykk5.js +0 -281
- package/dist/shared/components-7zkhz3r8.js +0 -399
- package/dist/shared/components-8v644a0s.js +0 -298
- package/dist/shared/components-a4evvq8h.js +0 -271
- package/dist/shared/components-adkarj8n.js +0 -35
- package/dist/shared/components-b1e239g3.js +0 -28
- package/dist/shared/components-b94t6qtj.js +0 -143
- package/dist/shared/components-c9yr4ed1.js +0 -44
- package/dist/shared/components-cwg1d0w6.js +0 -254
- package/dist/shared/components-d3shbwqq.js +0 -95
- package/dist/shared/components-dhgk6mbt.js +0 -16
- package/dist/shared/components-drxrtnp2.js +0 -25
- package/dist/shared/components-ej4r7wmj.js +0 -482
- package/dist/shared/components-f7h57m6t.js +0 -59
- package/dist/shared/components-fejdzvqc.js +0 -448
- package/dist/shared/components-gx4h5rbb.js +0 -40
- package/dist/shared/components-gyt7my41.js +0 -328
- package/dist/shared/components-h6p86be3.js +0 -19
- package/dist/shared/components-h83az0bz.js +0 -33
- package/dist/shared/components-hdzrqqpx.js +0 -55
- package/dist/shared/components-krtx3m5b.js +0 -84
- package/dist/shared/components-kswxnm3h.js +0 -24
- package/dist/shared/components-mpa2y6x8.js +0 -277
- package/dist/shared/components-mrbaxt8q.js +0 -62
- package/dist/shared/components-pt5ftm4p.js +0 -38
- package/dist/shared/components-rpw4md2b.js +0 -179
- package/dist/shared/components-rtadhd9j.js +0 -64
- package/dist/shared/components-tz4gaf1v.js +0 -23
- package/dist/shared/components-wbbckbw8.js +0 -34
- package/dist/shared/components-xdqmt6ed.js +0 -20
- package/dist/shared/components-xjkdvnbg.js +0 -107
- package/dist/shared/components-z3v0n0de.js +0 -11
- package/dist/shared/components-z6dfqtyt.js +0 -1545
- package/dist/tokens/colors.css +0 -230
- package/dist/tokens/motion.css +0 -40
- package/dist/tokens/radius.css +0 -10
- package/dist/tokens/spacing.css +0 -9
- package/dist/tokens/typography.css +0 -119
- package/docs/chrome.css +0 -120
- package/docs/chrome.tsx +0 -283
- package/docs/index.ts +0 -9
- package/docs/vite.ts +0 -85
- package/index.ts +0 -60
- package/lib/date-locale.ts +0 -195
- package/lib/field-classes.ts +0 -121
- package/lib/field-variants.ts +0 -117
- package/lib/fluid-hover-highlight.tsx +0 -139
- package/lib/focus-ring.tsx +0 -46
- package/lib/goo-indicator.tsx +0 -261
- package/lib/icon-context.tsx +0 -310
- package/lib/inert.ts +0 -22
- package/lib/morph-layers.tsx +0 -102
- package/lib/morph-part.tsx +0 -49
- package/lib/popup.ts +0 -52
- package/lib/reduced-motion.ts +0 -19
- package/lib/row-index.tsx +0 -29
- package/lib/row-order.tsx +0 -126
- package/lib/scroll-column.tsx +0 -273
- package/lib/shape-context.tsx +0 -138
- package/lib/shape-radius.ts +0 -67
- package/lib/size-context.tsx +0 -66
- package/lib/size-scale.ts +0 -115
- package/lib/slot.ts +0 -115
- package/lib/springs.ts +0 -62
- package/lib/surface-classes.ts +0 -58
- package/lib/surface-context.tsx +0 -21
- package/lib/use-controllable-state.ts +0 -44
- package/lib/use-fluid-hover.ts +0 -541
- package/lib/use-is-mobile.ts +0 -25
- package/lib/use-keyboard-nav-gate.ts +0 -45
- package/lib/use-merge-split.tsx +0 -209
- package/lib/use-morph.ts +0 -557
- package/lib/use-touch-primary.tsx +0 -43
- package/lib/utils.ts +0 -37
- package/preset.ts +0 -199
- package/primitives/code/examples.mdx +0 -27
- package/primitives/code/index.tsx +0 -80
- package/primitives/craft/demos.tsx +0 -173
- package/primitives/craft/examples.mdx +0 -142
- package/primitives/craft/index.tsx +0 -76
- package/primitives/fluid-hover/examples.mdx +0 -84
- package/primitives/fluid-hover/index.tsx +0 -316
- package/primitives/morph/examples.mdx +0 -139
- package/primitives/morph/index.tsx +0 -192
- package/primitives/motion/examples.mdx +0 -100
- package/primitives/motion/index.tsx +0 -160
- package/primitives/scroll-area/examples.mdx +0 -58
- package/primitives/scroll-area/index.tsx +0 -204
- package/primitives/separator/examples.mdx +0 -30
- package/primitives/separator/index.tsx +0 -53
- package/primitives/sizes/examples.mdx +0 -167
- package/primitives/sizes/index.tsx +0 -89
- package/primitives/skeleton/examples.mdx +0 -51
- package/primitives/skeleton/index.tsx +0 -72
- package/primitives/skeleton/skeleton.css +0 -15
- package/primitives/surface/examples.mdx +0 -173
- package/primitives/surface/index.tsx +0 -109
- package/primitives/surface/surface.css +0 -176
- /package/dist/shared/{components-3ey0hzpb.js → components-1v7rxytx.js} +0 -0
- /package/dist/shared/{components-jcnjay5g.js → components-ezxhrmer.js} +0 -0
|
@@ -1,1867 +0,0 @@
|
|
|
1
|
-
// biome-ignore-all lint: vendored upstream code keeps its own patterns (see the header)
|
|
2
|
-
/*
|
|
3
|
-
* Ported from Fluid Functionalism — github.com/mickadesign/fluid-functionalism
|
|
4
|
-
* `registry/base/slider.tsx` @ b3587bdbd83fc66c2a6aae3817ffb856cb09260b
|
|
5
|
-
* MIT License © 2026 Micka Touillaud — fluidfunctionalism.com (notice: LICENSE.fluid-functionalism)
|
|
6
|
-
* Local modifications:
|
|
7
|
-
* - `"use client"` directive dropped (no RSC here).
|
|
8
|
-
* - `framer-motion` → `motion/react`; `@/lib/*` imports rewritten to relative
|
|
9
|
-
* `../../../lib/*` paths.
|
|
10
|
-
* - `var(--color-accent)` → `var(--accent)` (the hover-preview fill of both
|
|
11
|
-
* designs; this package's tokens have no `--color-*` aliases).
|
|
12
|
-
* - `noUncheckedIndexedAccess` guards: non-null assertions on indexed reads
|
|
13
|
-
* of the value / step / pip arrays and the ResizeObserver entry (all
|
|
14
|
-
* in-bounds by construction — no behavior change).
|
|
15
|
-
* - React 18 ref types: ComfortableSlider's `containerRef` is
|
|
16
|
-
* `useRef<HTMLDivElement | null>` and a forwarded object ref is assigned
|
|
17
|
-
* through `MutableRefObject` (18's `RefObject.current` is readonly).
|
|
18
|
-
* - Uncontrolled support (local addition, Base UI style): the public
|
|
19
|
-
* `Slider`'s `value` / `onChange` are optional and a new `defaultValue`
|
|
20
|
-
* seeds internal state via the shared `useControllableState` (fallback:
|
|
21
|
-
* the lowest of `steps`, else `min`, else 0). With `value` passed it stays
|
|
22
|
-
* fully controlled, exactly as upstream. The two engines are unchanged and
|
|
23
|
-
* still receive a controlled `value` / `onChange` from the wrapper.
|
|
24
|
-
* - `SliderProps` extends `Omit<SliderEngineProps, "value" | "onChange">`
|
|
25
|
-
* (was `SliderEngineProps`) and re-declares the FF docs API-table props
|
|
26
|
-
* with their descriptions, so modo's parser lists them. The track-styling
|
|
27
|
-
* props (`trackClassName`, `trackStyle`, `fillClassName`, `fillStyle`,
|
|
28
|
-
* `hideFill`, `thumbColor`, `thumbBorderColor`) are still inherited.
|
|
29
|
-
* - `aria-label` (local addition) names the thumbs ahead of `label`, so a
|
|
30
|
-
* slider without a visible label prefix still has an accessible name; it
|
|
31
|
-
* is no longer spread onto the role-less root div.
|
|
32
|
-
* - `orientation="vertical"` (local addition): upright track on the compact engine
|
|
33
|
-
* (axis position grows from the bottom edge; pointer math reads clientY), and
|
|
34
|
-
* `onValueCommit` (both engines): fires once as a pointer drag ends, and for
|
|
35
|
-
* keyboard steps and committed edits through Base UI's `onValueCommitted`.
|
|
36
|
-
* - TSDoc with the FF docs page's examples added above `Slider`;
|
|
37
|
-
* `export default Slider` added.
|
|
38
|
-
* - Styling reads DS tokens (AGENTS.md styling): `text-[Npx]` →
|
|
39
|
-
* `text-<role>[-compact]`; inline `fontVariationSettings` → `weight-*`; the
|
|
40
|
-
* hex focus-ring fallback → `ring-focus-ring` / `border-focus-ring`;
|
|
41
|
-
* literal colors → color tokens.
|
|
42
|
-
*/
|
|
43
|
-
|
|
44
|
-
import { Slider as SliderPrimitive } from '@base-ui/react/slider'
|
|
45
|
-
import { AnimatePresence, animate, type MotionValue, motion, useMotionValue, useTransform } from 'motion/react'
|
|
46
|
-
import {
|
|
47
|
-
type CSSProperties,
|
|
48
|
-
forwardRef,
|
|
49
|
-
type HTMLAttributes,
|
|
50
|
-
useCallback,
|
|
51
|
-
useEffect,
|
|
52
|
-
useLayoutEffect,
|
|
53
|
-
useMemo,
|
|
54
|
-
useRef,
|
|
55
|
-
useState,
|
|
56
|
-
} from 'react'
|
|
57
|
-
import { useShape } from '../../../lib/shape-context'
|
|
58
|
-
import { type SizeVariant, useSizeVariant } from '../../../lib/size-context'
|
|
59
|
-
import { spring } from '../../../lib/springs'
|
|
60
|
-
import { useControllableState } from '../../../lib/use-controllable-state'
|
|
61
|
-
import { cn } from '../../../lib/utils'
|
|
62
|
-
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
// Types
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
|
|
67
|
-
type SliderValue = number | [number, number]
|
|
68
|
-
type ValuePosition = 'left' | 'right' | 'top' | 'bottom' | 'tooltip'
|
|
69
|
-
|
|
70
|
-
/** Props of the compact engine — the feature-rich design (ranges, discrete
|
|
71
|
-
* step lists, value display) that renders the ladder's compact step. The
|
|
72
|
-
* public <Slider> accepts these plus `variant` and `size`. */
|
|
73
|
-
interface SliderEngineProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue'> {
|
|
74
|
-
value: SliderValue
|
|
75
|
-
onChange: (value: SliderValue) => void
|
|
76
|
-
min?: number
|
|
77
|
-
max?: number
|
|
78
|
-
step?: number
|
|
79
|
-
/**
|
|
80
|
-
* Discrete list of allowed values, e.g. [0.1, 0.5, 0.7, 1.1, 1.3].
|
|
81
|
-
*
|
|
82
|
-
* When set, the thumb snaps only to these values (positioned proportionally
|
|
83
|
-
* along the track) and arrow keys walk the list. `min`/`max` derive from the
|
|
84
|
-
* list's extremes and `step` is ignored.
|
|
85
|
-
*/
|
|
86
|
-
steps?: number[]
|
|
87
|
-
showSteps?: boolean
|
|
88
|
-
showValue?: boolean
|
|
89
|
-
valuePosition?: ValuePosition
|
|
90
|
-
formatValue?: (v: number) => string
|
|
91
|
-
label?: string
|
|
92
|
-
disabled?: boolean
|
|
93
|
-
trackClassName?: string
|
|
94
|
-
trackStyle?: CSSProperties
|
|
95
|
-
fillClassName?: string
|
|
96
|
-
fillStyle?: CSSProperties
|
|
97
|
-
hideFill?: boolean
|
|
98
|
-
thumbColor?: string
|
|
99
|
-
thumbBorderColor?: string
|
|
100
|
-
orientation?: 'horizontal' | 'vertical'
|
|
101
|
-
onValueCommit?: (value: SliderValue) => void
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ---------------------------------------------------------------------------
|
|
105
|
-
// Constants
|
|
106
|
-
// ---------------------------------------------------------------------------
|
|
107
|
-
|
|
108
|
-
const THUMB_SIZE = 20
|
|
109
|
-
const THUMB_SIZE_REST = 16
|
|
110
|
-
const TRACK_BG_HEIGHT = 18
|
|
111
|
-
const DOT_SIZE = 4
|
|
112
|
-
const PIP_SIZE = 5
|
|
113
|
-
// Inset track BG so its rounded-end centers align with thumb centers at min/max
|
|
114
|
-
const TRACK_INSET = (THUMB_SIZE - TRACK_BG_HEIGHT) / 2
|
|
115
|
-
|
|
116
|
-
// ---------------------------------------------------------------------------
|
|
117
|
-
// Helpers
|
|
118
|
-
// ---------------------------------------------------------------------------
|
|
119
|
-
|
|
120
|
-
function valueToPixel(v: number, min: number, max: number, trackWidth: number): number {
|
|
121
|
-
if (max === min) return 0
|
|
122
|
-
const usable = trackWidth - THUMB_SIZE
|
|
123
|
-
return ((v - min) / (max - min)) * usable
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function nearestStepIndex(v: number, steps: number[]): number {
|
|
127
|
-
let idx = 0
|
|
128
|
-
for (let i = 1; i < steps.length; i++) {
|
|
129
|
-
if (Math.abs(steps[i]! - v) < Math.abs(steps[idx]! - v)) idx = i
|
|
130
|
-
}
|
|
131
|
-
return idx
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function pixelToValue(
|
|
135
|
-
px: number,
|
|
136
|
-
min: number,
|
|
137
|
-
max: number,
|
|
138
|
-
step: number,
|
|
139
|
-
trackWidth: number,
|
|
140
|
-
stepValues: number[] | null = null,
|
|
141
|
-
): number {
|
|
142
|
-
const usable = trackWidth - THUMB_SIZE
|
|
143
|
-
if (usable <= 0) return min
|
|
144
|
-
const raw = (px / usable) * (max - min) + min
|
|
145
|
-
if (stepValues) return stepValues[nearestStepIndex(raw, stepValues)]!
|
|
146
|
-
const snapped = Math.round((raw - min) / step) * step + min
|
|
147
|
-
return Math.max(min, Math.min(max, snapped))
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function toRadixValue(value: SliderValue): number[] {
|
|
151
|
-
return Array.isArray(value) ? value : [value]
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// ---------------------------------------------------------------------------
|
|
155
|
-
// ValueDisplay (internal)
|
|
156
|
-
// ---------------------------------------------------------------------------
|
|
157
|
-
|
|
158
|
-
interface ValueDisplayProps {
|
|
159
|
-
values: number[]
|
|
160
|
-
editingIndex: number | null
|
|
161
|
-
onStartEdit: (index: number) => void
|
|
162
|
-
onCommitEdit: (index: number, v: number) => void
|
|
163
|
-
onCancelEdit: () => void
|
|
164
|
-
min: number
|
|
165
|
-
max: number
|
|
166
|
-
step: number
|
|
167
|
-
stepValues: number[] | null
|
|
168
|
-
formatValue: (v: number) => string
|
|
169
|
-
label?: string
|
|
170
|
-
isRange: boolean
|
|
171
|
-
isInteracting: boolean
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function ValueDisplay({
|
|
175
|
-
values,
|
|
176
|
-
editingIndex,
|
|
177
|
-
onStartEdit,
|
|
178
|
-
onCommitEdit,
|
|
179
|
-
onCancelEdit,
|
|
180
|
-
min,
|
|
181
|
-
max,
|
|
182
|
-
step,
|
|
183
|
-
stepValues,
|
|
184
|
-
formatValue,
|
|
185
|
-
label,
|
|
186
|
-
isRange,
|
|
187
|
-
isInteracting,
|
|
188
|
-
}: ValueDisplayProps) {
|
|
189
|
-
const shape = useShape()
|
|
190
|
-
const [inputValue, setInputValue] = useState('')
|
|
191
|
-
const inputRef = useRef<HTMLInputElement>(null)
|
|
192
|
-
|
|
193
|
-
useEffect(() => {
|
|
194
|
-
if (editingIndex !== null) {
|
|
195
|
-
setInputValue(String(values[editingIndex]))
|
|
196
|
-
requestAnimationFrame(() => inputRef.current?.select())
|
|
197
|
-
}
|
|
198
|
-
}, [editingIndex])
|
|
199
|
-
|
|
200
|
-
const commitEdit = useCallback(
|
|
201
|
-
(index: number) => {
|
|
202
|
-
const parsed = parseFloat(inputValue)
|
|
203
|
-
if (!isNaN(parsed)) {
|
|
204
|
-
const clamped = Math.max(min, Math.min(max, parsed))
|
|
205
|
-
const snapped = stepValues
|
|
206
|
-
? stepValues[nearestStepIndex(clamped, stepValues)]!
|
|
207
|
-
: Math.round((clamped - min) / step) * step + min
|
|
208
|
-
onCommitEdit(index, snapped)
|
|
209
|
-
} else {
|
|
210
|
-
onCancelEdit()
|
|
211
|
-
}
|
|
212
|
-
},
|
|
213
|
-
[inputValue, min, max, step, stepValues, onCommitEdit, onCancelEdit],
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
const renderValue = (index: number) => {
|
|
217
|
-
if (editingIndex === index) {
|
|
218
|
-
return (
|
|
219
|
-
<span className="inline-grid text-body">
|
|
220
|
-
{/* Ghost for layout stability — widest possible value */}
|
|
221
|
-
<span className="col-start-1 row-start-1 invisible weight-medium" aria-hidden="true">
|
|
222
|
-
{label ? `${label}: ` : ''}
|
|
223
|
-
{formatValue(max)}
|
|
224
|
-
</span>
|
|
225
|
-
<span className="col-start-1 row-start-1 flex items-center gap-1">
|
|
226
|
-
{label && <span className="text-muted-foreground">{label}:</span>}
|
|
227
|
-
<input
|
|
228
|
-
ref={inputRef}
|
|
229
|
-
type="number"
|
|
230
|
-
value={inputValue}
|
|
231
|
-
min={min}
|
|
232
|
-
max={max}
|
|
233
|
-
step={stepValues ? 'any' : step}
|
|
234
|
-
onChange={e => setInputValue(e.target.value)}
|
|
235
|
-
onBlur={() => commitEdit(index)}
|
|
236
|
-
onKeyDown={e => {
|
|
237
|
-
if (e.key === 'Enter') commitEdit(index)
|
|
238
|
-
if (e.key === 'Escape') onCancelEdit()
|
|
239
|
-
}}
|
|
240
|
-
aria-label={`Edit slider value${isRange ? (index === 0 ? ' (start)' : ' (end)') : ''}`}
|
|
241
|
-
className={cn(
|
|
242
|
-
'w-[5ch] bg-transparent text-foreground outline-none border-b border-border text-center',
|
|
243
|
-
shape.input,
|
|
244
|
-
'weight-medium',
|
|
245
|
-
)}
|
|
246
|
-
/>
|
|
247
|
-
</span>
|
|
248
|
-
</span>
|
|
249
|
-
)
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return (
|
|
253
|
-
<span className="cursor-text select-none" onClick={() => onStartEdit(index)}>
|
|
254
|
-
{formatValue(values[index]!)}
|
|
255
|
-
</span>
|
|
256
|
-
)
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const widestValue = isRange
|
|
260
|
-
? `${label ? `${label}: ` : ''}${formatValue(max)} — ${formatValue(max)}`
|
|
261
|
-
: `${label ? `${label}: ` : ''}${formatValue(max)}`
|
|
262
|
-
|
|
263
|
-
return (
|
|
264
|
-
<span
|
|
265
|
-
className={cn(
|
|
266
|
-
'inline-grid shrink-0 text-body leading-none text-muted-foreground transition-[font-variation-settings] duration-100',
|
|
267
|
-
'tabular-nums',
|
|
268
|
-
isInteracting ? 'weight-medium' : 'weight-normal',
|
|
269
|
-
)}
|
|
270
|
-
>
|
|
271
|
-
{/* Invisible ghost — reserves width of widest possible value */}
|
|
272
|
-
<span className="col-start-1 row-start-1 invisible whitespace-nowrap weight-medium" aria-hidden="true">
|
|
273
|
-
{widestValue}
|
|
274
|
-
</span>
|
|
275
|
-
<span className="col-start-1 row-start-1 whitespace-nowrap">
|
|
276
|
-
{label && editingIndex === null && <span className="text-muted-foreground">{label}: </span>}
|
|
277
|
-
{isRange ? (
|
|
278
|
-
<>
|
|
279
|
-
{renderValue(0)}
|
|
280
|
-
<span className="mx-1 text-muted-foreground/50">—</span>
|
|
281
|
-
{renderValue(1)}
|
|
282
|
-
</>
|
|
283
|
-
) : (
|
|
284
|
-
renderValue(0)
|
|
285
|
-
)}
|
|
286
|
-
</span>
|
|
287
|
-
</span>
|
|
288
|
-
)
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// ---------------------------------------------------------------------------
|
|
292
|
-
// TooltipValue (internal)
|
|
293
|
-
// ---------------------------------------------------------------------------
|
|
294
|
-
|
|
295
|
-
interface TooltipValueProps {
|
|
296
|
-
value: number
|
|
297
|
-
formatValue: (v: number) => string
|
|
298
|
-
motionX: MotionValue<number>
|
|
299
|
-
vertical?: boolean
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function TooltipValue({ value, formatValue, motionX, vertical = false }: TooltipValueProps) {
|
|
303
|
-
const shape = useShape()
|
|
304
|
-
const tooltipX = useTransform(motionX, x => x + THUMB_SIZE / 2)
|
|
305
|
-
const tooltipY = useTransform(motionX, x => -x)
|
|
306
|
-
return (
|
|
307
|
-
<motion.div
|
|
308
|
-
className={cn('absolute pointer-events-none z-20', vertical ? 'translate-y-1/2' : '-translate-x-1/2')}
|
|
309
|
-
style={
|
|
310
|
-
vertical
|
|
311
|
-
? { y: tooltipY, bottom: THUMB_SIZE / 2, left: `calc(50% + ${THUMB_SIZE}px)` }
|
|
312
|
-
: { x: tooltipX, top: -16 }
|
|
313
|
-
}
|
|
314
|
-
initial={vertical ? { opacity: 0, x: -4 } : { opacity: 0, y: 4 }}
|
|
315
|
-
animate={vertical ? { opacity: 1, x: 0 } : { opacity: 1, y: 0 }}
|
|
316
|
-
exit={
|
|
317
|
-
vertical
|
|
318
|
-
? { opacity: 0, x: -4, transition: spring.fast.exit }
|
|
319
|
-
: { opacity: 0, y: 4, transition: spring.fast.exit }
|
|
320
|
-
}
|
|
321
|
-
transition={spring.fast}
|
|
322
|
-
>
|
|
323
|
-
<span
|
|
324
|
-
className={cn(
|
|
325
|
-
'text-caption text-background tabular-nums whitespace-nowrap bg-foreground px-2 py-1',
|
|
326
|
-
shape.bg,
|
|
327
|
-
'weight-medium',
|
|
328
|
-
)}
|
|
329
|
-
>
|
|
330
|
-
{formatValue(value)}
|
|
331
|
-
</span>
|
|
332
|
-
</motion.div>
|
|
333
|
-
)
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// ---------------------------------------------------------------------------
|
|
337
|
-
// CompactSlider — the compact-step design (formerly the only `Slider`).
|
|
338
|
-
// ---------------------------------------------------------------------------
|
|
339
|
-
|
|
340
|
-
const CompactSlider = forwardRef<HTMLDivElement, SliderEngineProps>(
|
|
341
|
-
(
|
|
342
|
-
{
|
|
343
|
-
value,
|
|
344
|
-
onChange,
|
|
345
|
-
min: minProp = 0,
|
|
346
|
-
max: maxProp = 100,
|
|
347
|
-
step = 1,
|
|
348
|
-
steps,
|
|
349
|
-
showSteps = false,
|
|
350
|
-
showValue = true,
|
|
351
|
-
valuePosition: valuePositionProp,
|
|
352
|
-
formatValue = String,
|
|
353
|
-
label,
|
|
354
|
-
disabled = false,
|
|
355
|
-
trackClassName,
|
|
356
|
-
trackStyle,
|
|
357
|
-
fillClassName,
|
|
358
|
-
fillStyle,
|
|
359
|
-
hideFill = false,
|
|
360
|
-
thumbColor,
|
|
361
|
-
thumbBorderColor,
|
|
362
|
-
orientation = 'horizontal',
|
|
363
|
-
onValueCommit,
|
|
364
|
-
className,
|
|
365
|
-
'aria-label': ariaLabel,
|
|
366
|
-
...props
|
|
367
|
-
},
|
|
368
|
-
ref,
|
|
369
|
-
) => {
|
|
370
|
-
const vertical = orientation === 'vertical'
|
|
371
|
-
const valuePosition = valuePositionProp ?? (vertical ? 'top' : 'left')
|
|
372
|
-
const onValueCommitRef = useRef(onValueCommit)
|
|
373
|
-
onValueCommitRef.current = onValueCommit
|
|
374
|
-
const latestValue = useRef<SliderValue>(value)
|
|
375
|
-
const isRange = Array.isArray(value)
|
|
376
|
-
const values = toRadixValue(value)
|
|
377
|
-
const shape = useShape()
|
|
378
|
-
|
|
379
|
-
// Non-uniform step mode: sorted, deduped list of allowed values. Keyed on
|
|
380
|
-
// the joined string so inline array literals don't recompute every render.
|
|
381
|
-
const stepsKey = steps ? steps.join(',') : ''
|
|
382
|
-
const stepValues = useMemo(() => {
|
|
383
|
-
if (!stepsKey) return null
|
|
384
|
-
const parsed = Array.from(new Set(stepsKey.split(',').map(Number))).sort((a, b) => a - b)
|
|
385
|
-
return parsed.length > 1 ? parsed : null
|
|
386
|
-
}, [stepsKey])
|
|
387
|
-
const min = stepValues ? stepValues[0]! : minProp
|
|
388
|
-
const max = stepValues ? stepValues[stepValues.length - 1]! : maxProp
|
|
389
|
-
|
|
390
|
-
// --- Refs ---
|
|
391
|
-
const trackRef = useRef<HTMLDivElement>(null)
|
|
392
|
-
const trackWidthRef = useRef(0)
|
|
393
|
-
const dragging = useRef(false)
|
|
394
|
-
const activeDragThumb = useRef<number>(0)
|
|
395
|
-
const valuesRef = useRef(values)
|
|
396
|
-
const minRef = useRef(min)
|
|
397
|
-
const maxRef = useRef(max)
|
|
398
|
-
valuesRef.current = values
|
|
399
|
-
minRef.current = min
|
|
400
|
-
maxRef.current = max
|
|
401
|
-
|
|
402
|
-
// --- State ---
|
|
403
|
-
const [isHovered, setIsHovered] = useState(false)
|
|
404
|
-
const [isPressed, setIsPressed] = useState(false)
|
|
405
|
-
const [editingIndex, setEditingIndex] = useState<number | null>(null)
|
|
406
|
-
const [hoverPreview, setHoverPreview] = useState<{
|
|
407
|
-
left: number
|
|
408
|
-
width: number
|
|
409
|
-
snappedValue: number
|
|
410
|
-
cursorX: number
|
|
411
|
-
} | null>(null)
|
|
412
|
-
const [focusedThumb, setFocusedThumb] = useState<number | null>(null)
|
|
413
|
-
const [showHoverTooltip, setShowHoverTooltip] = useState(false)
|
|
414
|
-
const hoverDelayRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
415
|
-
|
|
416
|
-
// Show hover tooltip after 100ms delay
|
|
417
|
-
useEffect(() => {
|
|
418
|
-
if (isHovered) {
|
|
419
|
-
hoverDelayRef.current = setTimeout(() => setShowHoverTooltip(true), 100)
|
|
420
|
-
} else {
|
|
421
|
-
if (hoverDelayRef.current) clearTimeout(hoverDelayRef.current)
|
|
422
|
-
setShowHoverTooltip(false)
|
|
423
|
-
}
|
|
424
|
-
return () => {
|
|
425
|
-
if (hoverDelayRef.current) clearTimeout(hoverDelayRef.current)
|
|
426
|
-
}
|
|
427
|
-
}, [isHovered])
|
|
428
|
-
|
|
429
|
-
// --- Motion values ---
|
|
430
|
-
const motionX0 = useMotionValue(0)
|
|
431
|
-
const motionX1 = useMotionValue(0)
|
|
432
|
-
|
|
433
|
-
// --- Derived motion values for fill ---
|
|
434
|
-
const fillLeft = useTransform(motionX0, x => (isRange ? x + THUMB_SIZE / 2 - TRACK_INSET : 0))
|
|
435
|
-
const fillWidthSingle = useTransform(motionX0, x => x + THUMB_SIZE / 2 - TRACK_INSET)
|
|
436
|
-
const fillWidthRange = useTransform(
|
|
437
|
-
[motionX0, motionX1] as MotionValue<number>[],
|
|
438
|
-
([x0, x1]) => (x1 as number) - (x0 as number),
|
|
439
|
-
)
|
|
440
|
-
const fillWidth = isRange ? fillWidthRange : fillWidthSingle
|
|
441
|
-
|
|
442
|
-
// Vertical: the axis position grows upward from the bottom edge.
|
|
443
|
-
const thumbY0 = useTransform(motionX0, x => -x)
|
|
444
|
-
const thumbY1 = useTransform(motionX1, x => -x)
|
|
445
|
-
|
|
446
|
-
// --- Step dots mask (hides dots on filled side, like SliderComfortable pips) ---
|
|
447
|
-
const maskDir = vertical ? 'to top' : 'to right'
|
|
448
|
-
const stepDotsMaskSingle = useTransform(motionX0, x => {
|
|
449
|
-
const edge = x + THUMB_SIZE / 2
|
|
450
|
-
return `linear-gradient(${maskDir}, transparent ${edge}px, black ${edge + 2}px)`
|
|
451
|
-
})
|
|
452
|
-
const stepDotsMaskRange = useTransform([motionX0, motionX1] as MotionValue<number>[], ([x0, x1]) => {
|
|
453
|
-
const left = (x0 as number) + THUMB_SIZE / 2
|
|
454
|
-
const right = (x1 as number) + THUMB_SIZE / 2
|
|
455
|
-
return `linear-gradient(${maskDir}, black ${left - 2}px, transparent ${left}px, transparent ${right}px, black ${right + 2}px)`
|
|
456
|
-
})
|
|
457
|
-
const stepDotsMask = isRange ? stepDotsMaskRange : stepDotsMaskSingle
|
|
458
|
-
|
|
459
|
-
// Pointer position along the track's axis (from the min end) and the track's length, in layout
|
|
460
|
-
// space so ancestor CSS scale doesn't skew them.
|
|
461
|
-
const measure = useCallback(
|
|
462
|
-
(clientX: number, clientY: number) => {
|
|
463
|
-
const trackEl = trackRef.current
|
|
464
|
-
if (!trackEl) return null
|
|
465
|
-
const rect = trackEl.getBoundingClientRect()
|
|
466
|
-
const length = vertical ? trackEl.offsetHeight : trackEl.offsetWidth
|
|
467
|
-
const visual = vertical ? rect.height : rect.width
|
|
468
|
-
if (length <= 0 || visual <= 0) return null
|
|
469
|
-
const scale = visual / length
|
|
470
|
-
return { length, pos: (vertical ? rect.bottom - clientY : clientX - rect.left) / scale }
|
|
471
|
-
},
|
|
472
|
-
[vertical],
|
|
473
|
-
)
|
|
474
|
-
|
|
475
|
-
// --- Hover preview computation ---
|
|
476
|
-
const computeHoverPreview = useCallback(
|
|
477
|
-
(cursorX: number, trackWidth: number) => {
|
|
478
|
-
// cursorX and trackWidth are in layout space (offsetWidth-relative),
|
|
479
|
-
// unaffected by ancestor CSS transforms. THUMB_SIZE / TRACK_INSET are
|
|
480
|
-
// also layout-space, so the math below is consistent end-to-end.
|
|
481
|
-
const usable = trackWidth - THUMB_SIZE
|
|
482
|
-
const rawPx = cursorX - THUMB_SIZE / 2
|
|
483
|
-
const clampedPx = Math.max(0, Math.min(usable, rawPx))
|
|
484
|
-
const rawVal = usable > 0 ? (clampedPx / usable) * (max - min) + min : min
|
|
485
|
-
const snappedVal = stepValues
|
|
486
|
-
? stepValues[nearestStepIndex(rawVal, stepValues)]!
|
|
487
|
-
: Math.max(min, Math.min(max, Math.round((rawVal - min) / step) * step + min))
|
|
488
|
-
const snappedPercent = max === min ? 0 : (snappedVal - min) / (max - min)
|
|
489
|
-
const snappedX = THUMB_SIZE / 2 + snappedPercent * usable
|
|
490
|
-
|
|
491
|
-
// Find nearest thumb center
|
|
492
|
-
const c0 = motionX0.get() + THUMB_SIZE / 2
|
|
493
|
-
const c1 = motionX1.get() + THUMB_SIZE / 2
|
|
494
|
-
const nearestIdx = isRange ? (Math.abs(snappedX - c0) <= Math.abs(snappedX - c1) ? 0 : 1) : 0
|
|
495
|
-
const nearest = nearestIdx === 0 ? c0 : c1
|
|
496
|
-
|
|
497
|
-
// Extend hover bar to track edges at extremes so there's no gap
|
|
498
|
-
const edgeX = snappedVal === min ? 0 : snappedVal === max ? trackWidth : snappedX
|
|
499
|
-
const left = Math.min(nearest, edgeX)
|
|
500
|
-
const width = Math.abs(edgeX - nearest)
|
|
501
|
-
setHoverPreview({ left, width, snappedValue: snappedVal, cursorX: snappedX })
|
|
502
|
-
},
|
|
503
|
-
[min, max, step, stepValues, isRange, motionX0, motionX1],
|
|
504
|
-
)
|
|
505
|
-
|
|
506
|
-
// --- Initial sync (before paint) ---
|
|
507
|
-
const initialSyncDone = useRef(false)
|
|
508
|
-
const [ready, setReady] = useState(false)
|
|
509
|
-
useLayoutEffect(() => {
|
|
510
|
-
const el = trackRef.current
|
|
511
|
-
if (!el || initialSyncDone.current) return
|
|
512
|
-
const w = vertical ? el.offsetHeight : el.offsetWidth
|
|
513
|
-
trackWidthRef.current = w
|
|
514
|
-
const px0 = valueToPixel(values[0]!, min, max, w)
|
|
515
|
-
motionX0.set(px0)
|
|
516
|
-
if (isRange && values[1] !== undefined) {
|
|
517
|
-
const px1 = valueToPixel(values[1]!, min, max, w)
|
|
518
|
-
motionX1.set(px1)
|
|
519
|
-
}
|
|
520
|
-
initialSyncDone.current = true
|
|
521
|
-
setReady(true)
|
|
522
|
-
}, [])
|
|
523
|
-
|
|
524
|
-
// --- Track width measurement (resize only) ---
|
|
525
|
-
useEffect(() => {
|
|
526
|
-
const el = trackRef.current
|
|
527
|
-
if (!el) return
|
|
528
|
-
const ro = new ResizeObserver(([entry]) => {
|
|
529
|
-
const w = vertical ? entry!.contentRect.height : entry!.contentRect.width
|
|
530
|
-
trackWidthRef.current = w
|
|
531
|
-
if (!dragging.current && initialSyncDone.current) {
|
|
532
|
-
const v = valuesRef.current
|
|
533
|
-
const mn = minRef.current
|
|
534
|
-
const mx = maxRef.current
|
|
535
|
-
const px0 = valueToPixel(v[0]!, mn, mx, w)
|
|
536
|
-
animate(motionX0, px0, spring.moderate)
|
|
537
|
-
if (isRange && v[1] !== undefined) {
|
|
538
|
-
const px1 = valueToPixel(v[1]!, mn, mx, w)
|
|
539
|
-
animate(motionX1, px1, spring.moderate)
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
})
|
|
543
|
-
ro.observe(el)
|
|
544
|
-
return () => ro.disconnect()
|
|
545
|
-
}, [isRange, vertical, motionX0, motionX1])
|
|
546
|
-
|
|
547
|
-
// --- Sync motion values on value change (keyboard, programmatic) ---
|
|
548
|
-
// Depend on a primitive key rather than the `values` array — its identity
|
|
549
|
-
// changes every render (toRadixValue allocates), which would restart the
|
|
550
|
-
// animation on unrelated re-renders (hover/tooltip state churn).
|
|
551
|
-
const valuesKey = values.join(',')
|
|
552
|
-
useEffect(() => {
|
|
553
|
-
if (!initialSyncDone.current) return
|
|
554
|
-
if (dragging.current) return
|
|
555
|
-
const tw = trackWidthRef.current
|
|
556
|
-
if (tw <= 0) return
|
|
557
|
-
const v = valuesRef.current
|
|
558
|
-
const px0 = valueToPixel(v[0]!, min, max, tw)
|
|
559
|
-
animate(motionX0, px0, spring.moderate)
|
|
560
|
-
if (isRange && v[1] !== undefined) {
|
|
561
|
-
const px1 = valueToPixel(v[1]!, min, max, tw)
|
|
562
|
-
animate(motionX1, px1, spring.moderate)
|
|
563
|
-
}
|
|
564
|
-
}, [valuesKey, min, max, isRange, motionX0, motionX1])
|
|
565
|
-
|
|
566
|
-
// --- Range crossing prevention ---
|
|
567
|
-
const clampForRange = useCallback(
|
|
568
|
-
(px: number, thumbIndex: number): number => {
|
|
569
|
-
if (!isRange) return px
|
|
570
|
-
if (thumbIndex === 0) {
|
|
571
|
-
return Math.min(px, motionX1.get() - THUMB_SIZE * 0.5)
|
|
572
|
-
}
|
|
573
|
-
return Math.max(px, motionX0.get() + THUMB_SIZE * 0.5)
|
|
574
|
-
},
|
|
575
|
-
[isRange, motionX0, motionX1],
|
|
576
|
-
)
|
|
577
|
-
|
|
578
|
-
// --- Emit value change ---
|
|
579
|
-
const emitChange = useCallback(
|
|
580
|
-
(thumbIndex: number, newValue: number) => {
|
|
581
|
-
if (isRange) {
|
|
582
|
-
const newValues: [number, number] = [...(values as [number, number])]
|
|
583
|
-
newValues[thumbIndex] = newValue
|
|
584
|
-
latestValue.current = newValues
|
|
585
|
-
onChange(newValues)
|
|
586
|
-
} else {
|
|
587
|
-
latestValue.current = newValue
|
|
588
|
-
onChange(newValue)
|
|
589
|
-
}
|
|
590
|
-
},
|
|
591
|
-
[isRange, values, onChange],
|
|
592
|
-
)
|
|
593
|
-
|
|
594
|
-
// --- Pointer handlers on track ---
|
|
595
|
-
const handlePointerDown = useCallback(
|
|
596
|
-
(e: React.PointerEvent<HTMLDivElement>) => {
|
|
597
|
-
if (disabled) return
|
|
598
|
-
if (e.pointerType === 'mouse' && e.button !== 0) return
|
|
599
|
-
e.preventDefault()
|
|
600
|
-
e.stopPropagation() // Prevent Radix from also handling the drag
|
|
601
|
-
|
|
602
|
-
// Normalized to layout space so it matches motionX (which is rendered as a
|
|
603
|
-
// CSS-pixel transform), even under ancestor CSS scale.
|
|
604
|
-
const m = measure(e.clientX, e.clientY)
|
|
605
|
-
if (!m) return
|
|
606
|
-
const layoutWidth = m.length
|
|
607
|
-
const localX = m.pos - THUMB_SIZE / 2
|
|
608
|
-
const clamped = Math.max(0, Math.min(layoutWidth - THUMB_SIZE, localX))
|
|
609
|
-
|
|
610
|
-
// Determine which thumb to drag
|
|
611
|
-
if (isRange) {
|
|
612
|
-
const dist0 = Math.abs(clamped - motionX0.get())
|
|
613
|
-
const dist1 = Math.abs(clamped - motionX1.get())
|
|
614
|
-
activeDragThumb.current = dist0 <= dist1 ? 0 : 1
|
|
615
|
-
} else {
|
|
616
|
-
activeDragThumb.current = 0
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
dragging.current = true
|
|
620
|
-
setIsPressed(true)
|
|
621
|
-
|
|
622
|
-
const motionX = activeDragThumb.current === 0 ? motionX0 : motionX1
|
|
623
|
-
|
|
624
|
-
// Snap to step grid immediately
|
|
625
|
-
const snappedValue = pixelToValue(clamped, min, max, step, layoutWidth, stepValues)
|
|
626
|
-
const snappedPx = valueToPixel(snappedValue, min, max, layoutWidth)
|
|
627
|
-
|
|
628
|
-
// Clamp for range crossing
|
|
629
|
-
const finalPx = clampForRange(snappedPx, activeDragThumb.current)
|
|
630
|
-
// Spring-animate thumb to clicked position
|
|
631
|
-
animate(motionX, finalPx, spring.moderate)
|
|
632
|
-
|
|
633
|
-
// Update value
|
|
634
|
-
const finalValue = pixelToValue(finalPx, min, max, step, layoutWidth, stepValues)
|
|
635
|
-
emitChange(activeDragThumb.current, finalValue)
|
|
636
|
-
|
|
637
|
-
;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
|
|
638
|
-
},
|
|
639
|
-
[disabled, isRange, min, max, step, stepValues, motionX0, motionX1, clampForRange, emitChange, measure],
|
|
640
|
-
)
|
|
641
|
-
|
|
642
|
-
const handlePointerMove = useCallback(
|
|
643
|
-
(e: React.PointerEvent<HTMLDivElement>) => {
|
|
644
|
-
if (!dragging.current) return
|
|
645
|
-
e.stopPropagation()
|
|
646
|
-
const m = measure(e.clientX, e.clientY)
|
|
647
|
-
if (!m) return
|
|
648
|
-
const layoutWidth = m.length
|
|
649
|
-
const localX = m.pos - THUMB_SIZE / 2
|
|
650
|
-
const clamped = Math.max(0, Math.min(layoutWidth - THUMB_SIZE, localX))
|
|
651
|
-
|
|
652
|
-
const motionX = activeDragThumb.current === 0 ? motionX0 : motionX1
|
|
653
|
-
|
|
654
|
-
// Snap to step grid during drag
|
|
655
|
-
const snappedValue = pixelToValue(clamped, min, max, step, layoutWidth, stepValues)
|
|
656
|
-
const snappedPx = valueToPixel(snappedValue, min, max, layoutWidth)
|
|
657
|
-
const finalPx = clampForRange(snappedPx, activeDragThumb.current)
|
|
658
|
-
motionX.set(finalPx)
|
|
659
|
-
|
|
660
|
-
const finalValue = pixelToValue(finalPx, min, max, step, layoutWidth, stepValues)
|
|
661
|
-
emitChange(activeDragThumb.current, finalValue)
|
|
662
|
-
},
|
|
663
|
-
[min, max, step, stepValues, motionX0, motionX1, clampForRange, emitChange, measure],
|
|
664
|
-
)
|
|
665
|
-
|
|
666
|
-
const handlePointerUp = useCallback(() => {
|
|
667
|
-
if (!dragging.current) return
|
|
668
|
-
dragging.current = false
|
|
669
|
-
onValueCommitRef.current?.(latestValue.current)
|
|
670
|
-
setIsPressed(false)
|
|
671
|
-
setHoverPreview(null)
|
|
672
|
-
|
|
673
|
-
// Spring settle to final quantized position
|
|
674
|
-
const tw = trackWidthRef.current
|
|
675
|
-
const motionX = activeDragThumb.current === 0 ? motionX0 : motionX1
|
|
676
|
-
const currentPx = motionX.get()
|
|
677
|
-
const snapped = pixelToValue(currentPx, min, max, step, tw, stepValues)
|
|
678
|
-
const snappedPx = valueToPixel(snapped, min, max, tw)
|
|
679
|
-
animate(motionX, snappedPx, spring.moderate)
|
|
680
|
-
}, [min, max, step, stepValues, motionX0, motionX1])
|
|
681
|
-
|
|
682
|
-
// --- Radix keyboard handler ---
|
|
683
|
-
// In steps mode the primitive runs on indices (0..len-1, step 1) so arrow
|
|
684
|
-
// keys walk the list; map indices back to actual values on the way out.
|
|
685
|
-
const handleRadixChange = useCallback(
|
|
686
|
-
(newValues: number[]) => {
|
|
687
|
-
if (dragging.current) return
|
|
688
|
-
const mapped = stepValues ? newValues.map(i => stepValues[Math.round(i)]!) : newValues
|
|
689
|
-
if (isRange) {
|
|
690
|
-
onChange(mapped as [number, number])
|
|
691
|
-
} else {
|
|
692
|
-
onChange(mapped[0]!)
|
|
693
|
-
}
|
|
694
|
-
},
|
|
695
|
-
[isRange, onChange, stepValues],
|
|
696
|
-
)
|
|
697
|
-
|
|
698
|
-
const handleRadixCommit = useCallback(
|
|
699
|
-
(newValues: number[]) => {
|
|
700
|
-
const mapped = stepValues ? newValues.map(i => stepValues[Math.round(i)]!) : newValues
|
|
701
|
-
onValueCommitRef.current?.(isRange ? (mapped as [number, number]) : mapped[0]!)
|
|
702
|
-
},
|
|
703
|
-
[isRange, stepValues],
|
|
704
|
-
)
|
|
705
|
-
|
|
706
|
-
// --- Click-to-edit handlers ---
|
|
707
|
-
const handleStartEdit = useCallback((index: number) => {
|
|
708
|
-
setEditingIndex(index)
|
|
709
|
-
}, [])
|
|
710
|
-
|
|
711
|
-
const handleCommitEdit = useCallback(
|
|
712
|
-
(index: number, v: number) => {
|
|
713
|
-
emitChange(index, v)
|
|
714
|
-
onValueCommitRef.current?.(latestValue.current)
|
|
715
|
-
setEditingIndex(null)
|
|
716
|
-
},
|
|
717
|
-
[emitChange],
|
|
718
|
-
)
|
|
719
|
-
|
|
720
|
-
const handleCancelEdit = useCallback(() => {
|
|
721
|
-
setEditingIndex(null)
|
|
722
|
-
}, [])
|
|
723
|
-
|
|
724
|
-
// --- Step dots ---
|
|
725
|
-
const stepDots = useMemo(
|
|
726
|
-
() =>
|
|
727
|
-
showSteps
|
|
728
|
-
? stepValues
|
|
729
|
-
? stepValues.map(v => ({
|
|
730
|
-
value: v,
|
|
731
|
-
percent: max === min ? 0 : (v - min) / (max - min),
|
|
732
|
-
}))
|
|
733
|
-
: Array.from({ length: Math.round((max - min) / step) + 1 }, (_, i) => {
|
|
734
|
-
const v = min + i * step
|
|
735
|
-
const percent = (v - min) / (max - min)
|
|
736
|
-
return { value: v, percent }
|
|
737
|
-
})
|
|
738
|
-
: [],
|
|
739
|
-
[showSteps, min, max, step, stepValues],
|
|
740
|
-
)
|
|
741
|
-
|
|
742
|
-
// --- Interaction state for tooltip ---
|
|
743
|
-
const isInteracting = isHovered || isPressed
|
|
744
|
-
|
|
745
|
-
// --- Per-thumb accessible names ---
|
|
746
|
-
// aria-label on Root lands on a role-less div and never reaches the
|
|
747
|
-
// thumb's input, so each Thumb gets its own label.
|
|
748
|
-
// `aria-label` names the thumbs ahead of the visible `label` (local addition).
|
|
749
|
-
const thumbAriaLabel = (index: number): string | undefined => {
|
|
750
|
-
const name = ariaLabel || label
|
|
751
|
-
if (!isRange) return name
|
|
752
|
-
if (!name) return index === 0 ? 'Minimum' : 'Maximum'
|
|
753
|
-
return index === 0 ? `${name} minimum` : `${name} maximum`
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// --- Value display component ---
|
|
757
|
-
const valueDisplay = showValue && valuePosition !== 'tooltip' && (
|
|
758
|
-
<ValueDisplay
|
|
759
|
-
values={values}
|
|
760
|
-
editingIndex={editingIndex}
|
|
761
|
-
onStartEdit={handleStartEdit}
|
|
762
|
-
onCommitEdit={handleCommitEdit}
|
|
763
|
-
onCancelEdit={handleCancelEdit}
|
|
764
|
-
min={min}
|
|
765
|
-
max={max}
|
|
766
|
-
step={step}
|
|
767
|
-
stepValues={stepValues}
|
|
768
|
-
formatValue={formatValue}
|
|
769
|
-
label={label}
|
|
770
|
-
isRange={isRange}
|
|
771
|
-
isInteracting={isInteracting}
|
|
772
|
-
/>
|
|
773
|
-
)
|
|
774
|
-
|
|
775
|
-
// --- Render visual thumb (not Radix — purely visual) ---
|
|
776
|
-
const renderVisualThumb = (index: number) => {
|
|
777
|
-
const motionX = index === 0 ? motionX0 : motionX1
|
|
778
|
-
return (
|
|
779
|
-
<motion.span
|
|
780
|
-
key={`visual-thumb-${index}`}
|
|
781
|
-
className="flex items-center justify-center pointer-events-none"
|
|
782
|
-
style={
|
|
783
|
-
vertical
|
|
784
|
-
? {
|
|
785
|
-
width: THUMB_SIZE,
|
|
786
|
-
height: THUMB_SIZE,
|
|
787
|
-
marginLeft: -THUMB_SIZE / 2,
|
|
788
|
-
y: index === 0 ? thumbY0 : thumbY1,
|
|
789
|
-
position: 'absolute',
|
|
790
|
-
left: '50%',
|
|
791
|
-
bottom: 0,
|
|
792
|
-
zIndex: 10,
|
|
793
|
-
}
|
|
794
|
-
: {
|
|
795
|
-
width: THUMB_SIZE,
|
|
796
|
-
height: THUMB_SIZE,
|
|
797
|
-
marginTop: -THUMB_SIZE / 2,
|
|
798
|
-
x: motionX,
|
|
799
|
-
position: 'absolute',
|
|
800
|
-
top: '50%',
|
|
801
|
-
left: 0,
|
|
802
|
-
zIndex: 10,
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
initial={false}
|
|
806
|
-
>
|
|
807
|
-
<motion.span
|
|
808
|
-
className={cn('block rounded-full shadow-thumb', !thumbColor && 'bg-thumb')}
|
|
809
|
-
initial={false}
|
|
810
|
-
animate={{
|
|
811
|
-
width: THUMB_SIZE_REST,
|
|
812
|
-
height: THUMB_SIZE_REST,
|
|
813
|
-
}}
|
|
814
|
-
transition={spring.fast}
|
|
815
|
-
style={{
|
|
816
|
-
backgroundColor: thumbColor,
|
|
817
|
-
border: thumbBorderColor ? `1px solid ${thumbBorderColor}` : undefined,
|
|
818
|
-
}}
|
|
819
|
-
/>
|
|
820
|
-
{/* Focus ring */}
|
|
821
|
-
<motion.span
|
|
822
|
-
className="absolute rounded-full border border-focus-ring pointer-events-none"
|
|
823
|
-
initial={false}
|
|
824
|
-
animate={{
|
|
825
|
-
opacity: focusedThumb === index ? 1 : 0,
|
|
826
|
-
width: THUMB_SIZE + 4,
|
|
827
|
-
height: THUMB_SIZE + 4,
|
|
828
|
-
}}
|
|
829
|
-
transition={spring.fast}
|
|
830
|
-
/>
|
|
831
|
-
</motion.span>
|
|
832
|
-
)
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
return (
|
|
836
|
-
<div
|
|
837
|
-
ref={ref}
|
|
838
|
-
className={cn(
|
|
839
|
-
'flex gap-0 select-none touch-none overflow-visible',
|
|
840
|
-
vertical ? 'h-40 w-fit items-center' : 'flex-col w-full',
|
|
841
|
-
valuePosition === 'left' || valuePosition === 'right'
|
|
842
|
-
? vertical
|
|
843
|
-
? 'flex-row gap-2'
|
|
844
|
-
: 'flex-row items-center gap-2 mb-2'
|
|
845
|
-
: 'flex-col',
|
|
846
|
-
vertical && (valuePosition === 'top' || valuePosition === 'bottom' || valuePosition === 'tooltip') && 'gap-2',
|
|
847
|
-
disabled && 'opacity-50 pointer-events-none',
|
|
848
|
-
className,
|
|
849
|
-
)}
|
|
850
|
-
{...props}
|
|
851
|
-
>
|
|
852
|
-
{/* Top / Left value */}
|
|
853
|
-
{(valuePosition === 'top' || valuePosition === 'left') && valueDisplay}
|
|
854
|
-
|
|
855
|
-
{/* Track area */}
|
|
856
|
-
<div
|
|
857
|
-
className={cn(
|
|
858
|
-
'relative overflow-visible',
|
|
859
|
-
vertical ? 'min-h-0 min-w-0' : 'flex-1',
|
|
860
|
-
vertical && (valuePosition === 'left' || valuePosition === 'right' ? 'h-full' : 'flex-1'),
|
|
861
|
-
)}
|
|
862
|
-
style={
|
|
863
|
-
vertical
|
|
864
|
-
? { width: THUMB_SIZE + 16 }
|
|
865
|
-
: {
|
|
866
|
-
height:
|
|
867
|
-
valuePosition === 'left' || valuePosition === 'right'
|
|
868
|
-
? THUMB_SIZE + 16
|
|
869
|
-
: THUMB_SIZE + (valuePosition === 'tooltip' ? 16 : 0),
|
|
870
|
-
paddingTop: valuePosition === 'tooltip' ? 16 : 0,
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
onPointerEnter={() => setIsHovered(true)}
|
|
874
|
-
onPointerLeave={() => {
|
|
875
|
-
setIsHovered(false)
|
|
876
|
-
setHoverPreview(null)
|
|
877
|
-
}}
|
|
878
|
-
onMouseMove={e => {
|
|
879
|
-
if (dragging.current) return
|
|
880
|
-
// Layout space, so the formula's THUMB_SIZE / TRACK_INSET constants (layout px)
|
|
881
|
-
// match the cursor's coordinate space, even when an ancestor applies a CSS
|
|
882
|
-
// scale transform (e.g. /demo).
|
|
883
|
-
const m = measure(e.clientX, e.clientY)
|
|
884
|
-
if (!m) return
|
|
885
|
-
computeHoverPreview(Math.max(0, Math.min(m.length, m.pos)), m.length)
|
|
886
|
-
}}
|
|
887
|
-
>
|
|
888
|
-
{/* Tooltip values */}
|
|
889
|
-
{showValue && valuePosition === 'tooltip' && (
|
|
890
|
-
<AnimatePresence>
|
|
891
|
-
{isInteracting && (
|
|
892
|
-
<TooltipValue
|
|
893
|
-
key="tooltip-0"
|
|
894
|
-
value={values[0]!}
|
|
895
|
-
formatValue={formatValue}
|
|
896
|
-
motionX={motionX0}
|
|
897
|
-
vertical={vertical}
|
|
898
|
-
/>
|
|
899
|
-
)}
|
|
900
|
-
{isInteracting && isRange && values[1] !== undefined && (
|
|
901
|
-
<TooltipValue
|
|
902
|
-
key="tooltip-1"
|
|
903
|
-
value={values[1]}
|
|
904
|
-
formatValue={formatValue}
|
|
905
|
-
motionX={motionX1}
|
|
906
|
-
vertical={vertical}
|
|
907
|
-
/>
|
|
908
|
-
)}
|
|
909
|
-
</AnimatePresence>
|
|
910
|
-
)}
|
|
911
|
-
|
|
912
|
-
{/* Base UI Slider — invisible, provides ARIA + keyboard nav */}
|
|
913
|
-
<SliderPrimitive.Root
|
|
914
|
-
value={stepValues ? values.map(v => nearestStepIndex(v, stepValues)) : values}
|
|
915
|
-
onValueChange={v => handleRadixChange(v as number[])}
|
|
916
|
-
onValueCommitted={v => handleRadixCommit(v as number[])}
|
|
917
|
-
min={stepValues ? 0 : min}
|
|
918
|
-
max={stepValues ? stepValues.length - 1 : max}
|
|
919
|
-
step={stepValues ? 1 : step}
|
|
920
|
-
orientation={orientation}
|
|
921
|
-
disabled={disabled}
|
|
922
|
-
className="absolute inset-0 opacity-0 pointer-events-none"
|
|
923
|
-
style={vertical ? { width: THUMB_SIZE } : { height: THUMB_SIZE }}
|
|
924
|
-
>
|
|
925
|
-
<SliderPrimitive.Control className="w-full h-full">
|
|
926
|
-
<SliderPrimitive.Track className="w-full h-full">
|
|
927
|
-
<SliderPrimitive.Indicator />
|
|
928
|
-
</SliderPrimitive.Track>
|
|
929
|
-
<SliderPrimitive.Thumb
|
|
930
|
-
index={0}
|
|
931
|
-
aria-label={thumbAriaLabel(0)}
|
|
932
|
-
getAriaValueText={stepValues ? () => formatValue(values[0]!) : undefined}
|
|
933
|
-
className="block outline-none"
|
|
934
|
-
style={{ width: THUMB_SIZE, height: THUMB_SIZE }}
|
|
935
|
-
onFocus={e => {
|
|
936
|
-
if ((e.target as HTMLElement).matches(':focus-visible')) setFocusedThumb(0)
|
|
937
|
-
}}
|
|
938
|
-
onBlur={() => setFocusedThumb(prev => (prev === 0 ? null : prev))}
|
|
939
|
-
/>
|
|
940
|
-
{isRange && (
|
|
941
|
-
<SliderPrimitive.Thumb
|
|
942
|
-
index={1}
|
|
943
|
-
aria-label={thumbAriaLabel(1)}
|
|
944
|
-
getAriaValueText={stepValues ? () => formatValue(values[1]!) : undefined}
|
|
945
|
-
className="block outline-none"
|
|
946
|
-
style={{ width: THUMB_SIZE, height: THUMB_SIZE }}
|
|
947
|
-
onFocus={e => {
|
|
948
|
-
if ((e.target as HTMLElement).matches(':focus-visible')) setFocusedThumb(1)
|
|
949
|
-
}}
|
|
950
|
-
onBlur={() => setFocusedThumb(prev => (prev === 1 ? null : prev))}
|
|
951
|
-
/>
|
|
952
|
-
)}
|
|
953
|
-
</SliderPrimitive.Control>
|
|
954
|
-
</SliderPrimitive.Root>
|
|
955
|
-
|
|
956
|
-
{/* Visual track with pointer handlers */}
|
|
957
|
-
<div
|
|
958
|
-
ref={trackRef}
|
|
959
|
-
className={cn('relative', vertical ? 'h-full cursor-ns-resize px-2' : 'w-full cursor-ew-resize py-2')}
|
|
960
|
-
style={
|
|
961
|
-
vertical
|
|
962
|
-
? { width: THUMB_SIZE + 16, opacity: ready ? 1 : 0 }
|
|
963
|
-
: { height: THUMB_SIZE + 16, opacity: ready ? 1 : 0 }
|
|
964
|
-
}
|
|
965
|
-
onPointerDown={handlePointerDown}
|
|
966
|
-
onPointerMove={handlePointerMove}
|
|
967
|
-
onPointerUp={handlePointerUp}
|
|
968
|
-
onPointerCancel={handlePointerUp}
|
|
969
|
-
>
|
|
970
|
-
{/* Extended hit area — 8px beyond each edge */}
|
|
971
|
-
<div
|
|
972
|
-
className={cn('absolute', vertical ? 'cursor-ns-resize' : 'cursor-ew-resize')}
|
|
973
|
-
style={vertical ? { left: 0, right: 0, top: -8, bottom: -8 } : { left: -8, right: -8, top: 0, bottom: 0 }}
|
|
974
|
-
onPointerDown={handlePointerDown}
|
|
975
|
-
onPointerMove={handlePointerMove}
|
|
976
|
-
onPointerUp={handlePointerUp}
|
|
977
|
-
onPointerCancel={handlePointerUp}
|
|
978
|
-
/>
|
|
979
|
-
{/* Hover value tooltip */}
|
|
980
|
-
<AnimatePresence>
|
|
981
|
-
{hoverPreview && showHoverTooltip && !isPressed && valuePosition !== 'tooltip' && (
|
|
982
|
-
<motion.div
|
|
983
|
-
key="hover-tooltip"
|
|
984
|
-
className={cn('absolute pointer-events-none z-20', vertical ? 'translate-y-1/2' : '-translate-x-1/2')}
|
|
985
|
-
initial={vertical ? { opacity: 0, x: -4 } : { opacity: 0, y: 4 }}
|
|
986
|
-
animate={vertical ? { opacity: 1, x: 0 } : { opacity: 1, y: 0 }}
|
|
987
|
-
exit={
|
|
988
|
-
vertical
|
|
989
|
-
? { opacity: 0, x: -4, transition: spring.fast.exit }
|
|
990
|
-
: { opacity: 0, y: 4, transition: spring.fast.exit }
|
|
991
|
-
}
|
|
992
|
-
transition={spring.fast}
|
|
993
|
-
style={
|
|
994
|
-
vertical
|
|
995
|
-
? { bottom: hoverPreview.cursorX, left: `calc(100% + 4px)` }
|
|
996
|
-
: { left: hoverPreview.cursorX, top: -20 }
|
|
997
|
-
}
|
|
998
|
-
>
|
|
999
|
-
<span
|
|
1000
|
-
className={cn(
|
|
1001
|
-
'text-caption text-background tabular-nums whitespace-nowrap bg-foreground px-2 py-1',
|
|
1002
|
-
shape.bg,
|
|
1003
|
-
'weight-medium',
|
|
1004
|
-
)}
|
|
1005
|
-
>
|
|
1006
|
-
{formatValue(hoverPreview.snappedValue)}
|
|
1007
|
-
</span>
|
|
1008
|
-
</motion.div>
|
|
1009
|
-
)}
|
|
1010
|
-
</AnimatePresence>
|
|
1011
|
-
|
|
1012
|
-
{/* Track background */}
|
|
1013
|
-
<motion.div
|
|
1014
|
-
className={cn(
|
|
1015
|
-
'absolute bg-transparent border border-border overflow-hidden rounded-full',
|
|
1016
|
-
trackClassName,
|
|
1017
|
-
)}
|
|
1018
|
-
initial={false}
|
|
1019
|
-
animate={
|
|
1020
|
-
vertical
|
|
1021
|
-
? { width: TRACK_BG_HEIGHT, left: 8 + (THUMB_SIZE - TRACK_BG_HEIGHT) / 2 }
|
|
1022
|
-
: { height: TRACK_BG_HEIGHT, top: 8 + (THUMB_SIZE - TRACK_BG_HEIGHT) / 2 }
|
|
1023
|
-
}
|
|
1024
|
-
transition={spring.fast}
|
|
1025
|
-
style={{
|
|
1026
|
-
...(vertical ? { top: TRACK_INSET, bottom: TRACK_INSET } : { left: TRACK_INSET, right: TRACK_INSET }),
|
|
1027
|
-
...trackStyle,
|
|
1028
|
-
}}
|
|
1029
|
-
>
|
|
1030
|
-
{/* Filled range */}
|
|
1031
|
-
{!hideFill && (
|
|
1032
|
-
<motion.div
|
|
1033
|
-
className={cn(
|
|
1034
|
-
'absolute bg-selected/50 dark:bg-accent/40',
|
|
1035
|
-
vertical ? 'w-full' : 'h-full',
|
|
1036
|
-
fillClassName,
|
|
1037
|
-
)}
|
|
1038
|
-
style={{
|
|
1039
|
-
...(vertical ? { bottom: fillLeft, height: fillWidth } : { left: fillLeft, width: fillWidth }),
|
|
1040
|
-
...fillStyle,
|
|
1041
|
-
}}
|
|
1042
|
-
/>
|
|
1043
|
-
)}
|
|
1044
|
-
|
|
1045
|
-
{/* Hover preview */}
|
|
1046
|
-
<motion.div
|
|
1047
|
-
className={cn('absolute bg-accent/40 pointer-events-none z-[2]', vertical ? 'w-full' : 'h-full')}
|
|
1048
|
-
initial={false}
|
|
1049
|
-
animate={{
|
|
1050
|
-
opacity: hoverPreview && !isPressed ? 1 : 0,
|
|
1051
|
-
}}
|
|
1052
|
-
transition={{
|
|
1053
|
-
opacity: { duration: 0.15 },
|
|
1054
|
-
}}
|
|
1055
|
-
style={{
|
|
1056
|
-
...(vertical
|
|
1057
|
-
? {
|
|
1058
|
-
bottom: hoverPreview ? hoverPreview.left - TRACK_INSET : 0,
|
|
1059
|
-
height: hoverPreview ? hoverPreview.width : 0,
|
|
1060
|
-
}
|
|
1061
|
-
: {
|
|
1062
|
-
left: hoverPreview ? hoverPreview.left - TRACK_INSET : 0,
|
|
1063
|
-
width: hoverPreview ? hoverPreview.width : 0,
|
|
1064
|
-
}),
|
|
1065
|
-
borderRadius:
|
|
1066
|
-
hoverPreview && hoverPreview.cursorX > hoverPreview.left
|
|
1067
|
-
? vertical
|
|
1068
|
-
? '9999px 9999px 0 0'
|
|
1069
|
-
: '0 9999px 9999px 0'
|
|
1070
|
-
: vertical
|
|
1071
|
-
? '0 0 9999px 9999px'
|
|
1072
|
-
: '9999px 0 0 9999px',
|
|
1073
|
-
}}
|
|
1074
|
-
/>
|
|
1075
|
-
</motion.div>
|
|
1076
|
-
|
|
1077
|
-
{/* Step dots — masked so filled side is hidden */}
|
|
1078
|
-
{stepDots.length > 0 && (
|
|
1079
|
-
<motion.div
|
|
1080
|
-
className={cn('absolute pointer-events-none', vertical ? 'top-0 bottom-0' : 'left-0 right-0')}
|
|
1081
|
-
style={{
|
|
1082
|
-
...(vertical
|
|
1083
|
-
? { left: 8 + (THUMB_SIZE - TRACK_BG_HEIGHT) / 2, width: TRACK_BG_HEIGHT }
|
|
1084
|
-
: { top: 8 + (THUMB_SIZE - TRACK_BG_HEIGHT) / 2, height: TRACK_BG_HEIGHT }),
|
|
1085
|
-
WebkitMaskImage: stepDotsMask,
|
|
1086
|
-
maskImage: stepDotsMask,
|
|
1087
|
-
}}
|
|
1088
|
-
>
|
|
1089
|
-
{stepDots.map(({ value: v, percent }) => (
|
|
1090
|
-
<div
|
|
1091
|
-
key={v}
|
|
1092
|
-
className="absolute pointer-events-none flex items-center justify-center"
|
|
1093
|
-
style={{
|
|
1094
|
-
...(vertical
|
|
1095
|
-
? {
|
|
1096
|
-
bottom: `calc(${THUMB_SIZE / 2}px + ${percent} * (100% - ${THUMB_SIZE}px))`,
|
|
1097
|
-
left: '50%',
|
|
1098
|
-
}
|
|
1099
|
-
: {
|
|
1100
|
-
left: `calc(${THUMB_SIZE / 2}px + ${percent} * (100% - ${THUMB_SIZE}px))`,
|
|
1101
|
-
top: '50%',
|
|
1102
|
-
}),
|
|
1103
|
-
width: 0,
|
|
1104
|
-
height: 0,
|
|
1105
|
-
}}
|
|
1106
|
-
>
|
|
1107
|
-
<motion.div
|
|
1108
|
-
className="rounded-full flex-shrink-0 bg-muted-foreground opacity-30"
|
|
1109
|
-
initial={false}
|
|
1110
|
-
animate={{
|
|
1111
|
-
width: isHovered ? DOT_SIZE * 1.25 : DOT_SIZE,
|
|
1112
|
-
height: isHovered ? DOT_SIZE * 1.25 : DOT_SIZE,
|
|
1113
|
-
}}
|
|
1114
|
-
transition={spring.moderate}
|
|
1115
|
-
/>
|
|
1116
|
-
</div>
|
|
1117
|
-
))}
|
|
1118
|
-
</motion.div>
|
|
1119
|
-
)}
|
|
1120
|
-
|
|
1121
|
-
{/* Visual thumbs */}
|
|
1122
|
-
{renderVisualThumb(0)}
|
|
1123
|
-
{isRange && renderVisualThumb(1)}
|
|
1124
|
-
</div>
|
|
1125
|
-
</div>
|
|
1126
|
-
|
|
1127
|
-
{/* Bottom / Right value */}
|
|
1128
|
-
{(valuePosition === 'bottom' || valuePosition === 'right') && valueDisplay}
|
|
1129
|
-
</div>
|
|
1130
|
-
)
|
|
1131
|
-
},
|
|
1132
|
-
)
|
|
1133
|
-
|
|
1134
|
-
CompactSlider.displayName = 'SliderCompact'
|
|
1135
|
-
|
|
1136
|
-
// ---------------------------------------------------------------------------
|
|
1137
|
-
// ComfortableSlider — the default-step design (pips / scrubber layouts).
|
|
1138
|
-
// ---------------------------------------------------------------------------
|
|
1139
|
-
|
|
1140
|
-
interface SliderComfortableProps
|
|
1141
|
-
extends Omit<
|
|
1142
|
-
HTMLAttributes<HTMLDivElement>,
|
|
1143
|
-
'onChange' | 'defaultValue' | 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onDragOver' | 'onAnimationStart'
|
|
1144
|
-
> {
|
|
1145
|
-
value: number
|
|
1146
|
-
onChange: (value: number) => void
|
|
1147
|
-
min?: number
|
|
1148
|
-
max?: number
|
|
1149
|
-
step?: number
|
|
1150
|
-
variant?: 'pips' | 'scrubber'
|
|
1151
|
-
label?: string
|
|
1152
|
-
formatValue?: (v: number) => string
|
|
1153
|
-
disabled?: boolean
|
|
1154
|
-
onValueCommit?: (value: number) => void
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
const ComfortableSlider = forwardRef<HTMLDivElement, SliderComfortableProps>(
|
|
1158
|
-
(
|
|
1159
|
-
{
|
|
1160
|
-
value,
|
|
1161
|
-
onChange,
|
|
1162
|
-
min = 0,
|
|
1163
|
-
max = 100,
|
|
1164
|
-
step = 1,
|
|
1165
|
-
variant = 'pips',
|
|
1166
|
-
label,
|
|
1167
|
-
formatValue = String,
|
|
1168
|
-
disabled = false,
|
|
1169
|
-
onValueCommit,
|
|
1170
|
-
className,
|
|
1171
|
-
'aria-label': ariaLabel,
|
|
1172
|
-
...props
|
|
1173
|
-
},
|
|
1174
|
-
ref,
|
|
1175
|
-
) => {
|
|
1176
|
-
const containerRef = useRef<HTMLDivElement | null>(null)
|
|
1177
|
-
const onValueCommitRef = useRef(onValueCommit)
|
|
1178
|
-
onValueCommitRef.current = onValueCommit
|
|
1179
|
-
const latestValue = useRef(value)
|
|
1180
|
-
const dragging = useRef(false)
|
|
1181
|
-
const handleDragging = useRef(false)
|
|
1182
|
-
const [isHovered, setIsHovered] = useState(false)
|
|
1183
|
-
const [isPressed, setIsPressed] = useState(false)
|
|
1184
|
-
const [isFocused, setIsFocused] = useState(false)
|
|
1185
|
-
const [hoverPreview, setHoverPreview] = useState<{
|
|
1186
|
-
left: number
|
|
1187
|
-
width: number
|
|
1188
|
-
snappedValue: number
|
|
1189
|
-
cursorX: number
|
|
1190
|
-
} | null>(null)
|
|
1191
|
-
const [showHoverTooltip, setShowHoverTooltip] = useState(false)
|
|
1192
|
-
const hoverDelayRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
1193
|
-
const shape = useShape()
|
|
1194
|
-
|
|
1195
|
-
// Show hover tooltip after 100ms delay
|
|
1196
|
-
useEffect(() => {
|
|
1197
|
-
if (isHovered) {
|
|
1198
|
-
hoverDelayRef.current = setTimeout(() => setShowHoverTooltip(true), 100)
|
|
1199
|
-
} else {
|
|
1200
|
-
if (hoverDelayRef.current) clearTimeout(hoverDelayRef.current)
|
|
1201
|
-
setShowHoverTooltip(false)
|
|
1202
|
-
}
|
|
1203
|
-
return () => {
|
|
1204
|
-
if (hoverDelayRef.current) clearTimeout(hoverDelayRef.current)
|
|
1205
|
-
}
|
|
1206
|
-
}, [isHovered])
|
|
1207
|
-
|
|
1208
|
-
const mergedRef = useCallback(
|
|
1209
|
-
(el: HTMLDivElement | null) => {
|
|
1210
|
-
containerRef.current = el
|
|
1211
|
-
if (typeof ref === 'function') (ref as React.RefCallback<HTMLDivElement>)(el)
|
|
1212
|
-
else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = el
|
|
1213
|
-
},
|
|
1214
|
-
[ref],
|
|
1215
|
-
)
|
|
1216
|
-
|
|
1217
|
-
const pipSteps = useMemo(
|
|
1218
|
-
() => Array.from({ length: Math.round((max - min) / step) + 1 }, (_, i) => min + i * step),
|
|
1219
|
-
[min, max, step],
|
|
1220
|
-
)
|
|
1221
|
-
const pipCount = pipSteps.length
|
|
1222
|
-
|
|
1223
|
-
// Fill motion value
|
|
1224
|
-
const fillPercent = useMotionValue(max === min ? 0 : Math.max(0, Math.min(1, (value - min) / (max - min))))
|
|
1225
|
-
// Small offset when value is at min so the handle line stays visible
|
|
1226
|
-
const zeroTarget = variant === 'pips' ? 8 : 17
|
|
1227
|
-
const zeroOffset = useMotionValue(value === min ? zeroTarget : 0)
|
|
1228
|
-
|
|
1229
|
-
const fillWidthStyle = useTransform(fillPercent, p => `${p * 100}%`)
|
|
1230
|
-
const handleLeftStyle = useTransform(
|
|
1231
|
-
[fillPercent, zeroOffset] as MotionValue<number>[],
|
|
1232
|
-
([p, zo]) => `calc(${(p as number) * 100}% - 8px + ${zo as number}px)`,
|
|
1233
|
-
)
|
|
1234
|
-
const handleLineLeftStyle = useTransform(
|
|
1235
|
-
[fillPercent, zeroOffset] as MotionValue<number>[],
|
|
1236
|
-
([p, zo]) => `calc(${(p as number) * 100}% - 9px + ${zo as number}px)`,
|
|
1237
|
-
)
|
|
1238
|
-
// Pips-specific: offset by px-3 (12px) padding so fill edge aligns with active pip center
|
|
1239
|
-
const pipsFillWidthStyle = useTransform(
|
|
1240
|
-
[fillPercent, zeroOffset] as MotionValue<number>[],
|
|
1241
|
-
([p, zo]) => `calc(${(p as number) * 100}% + ${20 - 20 * (p as number) - (zo as number) * 2.5}px)`,
|
|
1242
|
-
)
|
|
1243
|
-
const pipsHandleLineLeftStyle = useTransform(fillPercent, p => `calc(${p * 100}% + ${11 - 24 * p}px)`)
|
|
1244
|
-
const pipsMaskStyle = useTransform([fillPercent, zeroOffset] as MotionValue<number>[], ([p, zo]) => {
|
|
1245
|
-
const offset = 20 - 20 * (p as number) - (zo as number) * 2.5
|
|
1246
|
-
return `linear-gradient(to right, transparent calc(${(p as number) * 100}% + ${offset}px), black calc(${(p as number) * 100}% + ${offset + 2}px))`
|
|
1247
|
-
})
|
|
1248
|
-
|
|
1249
|
-
// --- Hover preview computation ---
|
|
1250
|
-
const computeHoverPreview = useCallback(
|
|
1251
|
-
(clientX: number) => {
|
|
1252
|
-
const el = containerRef.current
|
|
1253
|
-
if (!el) return
|
|
1254
|
-
const rect = el.getBoundingClientRect()
|
|
1255
|
-
// Use clientWidth (padding box) — CSS % and absolute left/width are relative to it
|
|
1256
|
-
const w = el.clientWidth
|
|
1257
|
-
if (w <= 0 || rect.width <= 0) return
|
|
1258
|
-
// Normalize cursor to layout space so it matches `w` (layout, padding
|
|
1259
|
-
// box). offsetWidth is the layout border-box; the difference vs `w` is
|
|
1260
|
-
// the horizontal border contribution split across both sides.
|
|
1261
|
-
const scale = rect.width / el.offsetWidth
|
|
1262
|
-
const borderLeftLayout = (el.offsetWidth - w) / 2
|
|
1263
|
-
const visualX = clientX - rect.left
|
|
1264
|
-
const layoutX = visualX / scale - borderLeftLayout
|
|
1265
|
-
const clamped = Math.max(0, Math.min(w, layoutX))
|
|
1266
|
-
|
|
1267
|
-
// Snap to nearest step value
|
|
1268
|
-
let snappedVal: number
|
|
1269
|
-
if (variant === 'pips') {
|
|
1270
|
-
if (pipCount <= 1) return
|
|
1271
|
-
const index = Math.max(0, Math.min(pipCount - 1, Math.round((clamped / w) * (pipCount - 1))))
|
|
1272
|
-
snappedVal = pipSteps[index]!
|
|
1273
|
-
} else {
|
|
1274
|
-
const raw = min + (clamped / w) * (max - min)
|
|
1275
|
-
snappedVal = Math.max(min, Math.min(max, Math.round((raw - min) / step) * step + min))
|
|
1276
|
-
}
|
|
1277
|
-
const snappedPercent = max === min ? 0 : (snappedVal - min) / (max - min)
|
|
1278
|
-
const snappedX = snappedPercent * w
|
|
1279
|
-
|
|
1280
|
-
// Current handle position — for pips, match the visual fill edge offset
|
|
1281
|
-
const currentPercent = fillPercent.get()
|
|
1282
|
-
let handleX: number
|
|
1283
|
-
if (variant === 'pips') {
|
|
1284
|
-
const zo = zeroOffset.get()
|
|
1285
|
-
handleX = currentPercent * w + (20 - 20 * currentPercent - zo * 2.5)
|
|
1286
|
-
} else {
|
|
1287
|
-
handleX = currentPercent * w
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
// Extend hover bar to container edges at extremes so there's no gap
|
|
1291
|
-
const edgeX = snappedVal === min ? 0 : snappedVal === max ? w : snappedX
|
|
1292
|
-
const left = Math.min(handleX, edgeX)
|
|
1293
|
-
const width = Math.abs(edgeX - handleX)
|
|
1294
|
-
setHoverPreview({ left, width, snappedValue: snappedVal, cursorX: snappedX })
|
|
1295
|
-
},
|
|
1296
|
-
[variant, pipSteps, pipCount, min, max, step, fillPercent, zeroOffset],
|
|
1297
|
-
)
|
|
1298
|
-
|
|
1299
|
-
// Sync fill on programmatic value change
|
|
1300
|
-
useEffect(() => {
|
|
1301
|
-
if (dragging.current || handleDragging.current) return
|
|
1302
|
-
const percent = max === min ? 0 : Math.max(0, Math.min(1, (value - min) / (max - min)))
|
|
1303
|
-
animate(fillPercent, percent, spring.fast)
|
|
1304
|
-
animate(zeroOffset, value === min ? zeroTarget : 0, spring.fast)
|
|
1305
|
-
}, [value, min, max, variant, fillPercent, zeroOffset, zeroTarget])
|
|
1306
|
-
|
|
1307
|
-
const getValueFromX = useCallback(
|
|
1308
|
-
(clientX: number) => {
|
|
1309
|
-
const rect = containerRef.current?.getBoundingClientRect()
|
|
1310
|
-
if (!rect) return min
|
|
1311
|
-
const x = clientX - rect.left
|
|
1312
|
-
const clamped = Math.max(0, Math.min(rect.width, x))
|
|
1313
|
-
if (variant === 'pips') {
|
|
1314
|
-
if (pipCount <= 1) return min
|
|
1315
|
-
const index = Math.max(0, Math.min(pipCount - 1, Math.round((clamped / rect.width) * (pipCount - 1))))
|
|
1316
|
-
return pipSteps[index]!
|
|
1317
|
-
}
|
|
1318
|
-
const raw = min + (clamped / rect.width) * (max - min)
|
|
1319
|
-
const snapped = Math.round((raw - min) / step) * step + min
|
|
1320
|
-
return Math.max(min, Math.min(max, snapped))
|
|
1321
|
-
},
|
|
1322
|
-
[variant, pipSteps, pipCount, min, max, step],
|
|
1323
|
-
)
|
|
1324
|
-
|
|
1325
|
-
const handlePointerDown = useCallback(
|
|
1326
|
-
(e: React.PointerEvent<HTMLDivElement>) => {
|
|
1327
|
-
if (disabled) return
|
|
1328
|
-
if (e.pointerType === 'mouse' && e.button !== 0) return
|
|
1329
|
-
e.preventDefault()
|
|
1330
|
-
dragging.current = true
|
|
1331
|
-
setIsPressed(true)
|
|
1332
|
-
const newVal = getValueFromX(e.clientX)
|
|
1333
|
-
latestValue.current = newVal
|
|
1334
|
-
onChange(newVal)
|
|
1335
|
-
const newPercent = Math.max(0, Math.min(1, (newVal - min) / (max - min)))
|
|
1336
|
-
animate(fillPercent, newPercent, spring.fast)
|
|
1337
|
-
animate(zeroOffset, newVal === min ? zeroTarget : 0, spring.fast)
|
|
1338
|
-
;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
|
|
1339
|
-
},
|
|
1340
|
-
[disabled, getValueFromX, onChange, fillPercent, zeroOffset, zeroTarget, min, max],
|
|
1341
|
-
)
|
|
1342
|
-
|
|
1343
|
-
const handlePointerMove = useCallback(
|
|
1344
|
-
(e: React.PointerEvent<HTMLDivElement>) => {
|
|
1345
|
-
if (!dragging.current) return
|
|
1346
|
-
const newVal = getValueFromX(e.clientX)
|
|
1347
|
-
latestValue.current = newVal
|
|
1348
|
-
onChange(newVal)
|
|
1349
|
-
const newPercent = Math.max(0, Math.min(1, (newVal - min) / (max - min)))
|
|
1350
|
-
if (variant === 'scrubber') {
|
|
1351
|
-
fillPercent.set(newPercent)
|
|
1352
|
-
} else {
|
|
1353
|
-
animate(fillPercent, newPercent, spring.fast)
|
|
1354
|
-
}
|
|
1355
|
-
animate(zeroOffset, newVal === min ? zeroTarget : 0, spring.fast)
|
|
1356
|
-
},
|
|
1357
|
-
[getValueFromX, onChange, variant, fillPercent, zeroOffset, zeroTarget, min, max],
|
|
1358
|
-
)
|
|
1359
|
-
|
|
1360
|
-
const handlePointerUp = useCallback(() => {
|
|
1361
|
-
if (dragging.current) onValueCommitRef.current?.(latestValue.current)
|
|
1362
|
-
dragging.current = false
|
|
1363
|
-
setIsPressed(false)
|
|
1364
|
-
setHoverPreview(null)
|
|
1365
|
-
}, [])
|
|
1366
|
-
|
|
1367
|
-
// Resize handle drag handlers (direct cursor position)
|
|
1368
|
-
const handleResizePointerDown = useCallback(
|
|
1369
|
-
(e: React.PointerEvent<HTMLDivElement>) => {
|
|
1370
|
-
if (disabled) return
|
|
1371
|
-
if (e.pointerType === 'mouse' && e.button !== 0) return
|
|
1372
|
-
e.preventDefault()
|
|
1373
|
-
e.stopPropagation()
|
|
1374
|
-
handleDragging.current = true
|
|
1375
|
-
setIsPressed(true)
|
|
1376
|
-
const newVal = getValueFromX(e.clientX)
|
|
1377
|
-
latestValue.current = newVal
|
|
1378
|
-
onChange(newVal)
|
|
1379
|
-
fillPercent.set(Math.max(0, Math.min(1, (newVal - min) / (max - min))))
|
|
1380
|
-
animate(zeroOffset, newVal === min ? zeroTarget : 0, spring.fast)
|
|
1381
|
-
;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
|
|
1382
|
-
},
|
|
1383
|
-
[disabled, getValueFromX, onChange, fillPercent, zeroOffset, zeroTarget, min, max],
|
|
1384
|
-
)
|
|
1385
|
-
|
|
1386
|
-
const handleResizePointerMove = useCallback(
|
|
1387
|
-
(e: React.PointerEvent<HTMLDivElement>) => {
|
|
1388
|
-
if (!handleDragging.current) return
|
|
1389
|
-
const newVal = getValueFromX(e.clientX)
|
|
1390
|
-
latestValue.current = newVal
|
|
1391
|
-
onChange(newVal)
|
|
1392
|
-
fillPercent.set(Math.max(0, Math.min(1, (newVal - min) / (max - min))))
|
|
1393
|
-
animate(zeroOffset, newVal === min ? zeroTarget : 0, spring.fast)
|
|
1394
|
-
},
|
|
1395
|
-
[getValueFromX, onChange, fillPercent, zeroOffset, zeroTarget, min, max],
|
|
1396
|
-
)
|
|
1397
|
-
|
|
1398
|
-
const handleResizePointerUp = useCallback(() => {
|
|
1399
|
-
if (handleDragging.current) onValueCommitRef.current?.(latestValue.current)
|
|
1400
|
-
handleDragging.current = false
|
|
1401
|
-
setIsPressed(false)
|
|
1402
|
-
setHoverPreview(null)
|
|
1403
|
-
}, [])
|
|
1404
|
-
|
|
1405
|
-
const handleRadixChange = useCallback(
|
|
1406
|
-
(newValues: number[]) => {
|
|
1407
|
-
onChange(newValues[0]!)
|
|
1408
|
-
},
|
|
1409
|
-
[onChange],
|
|
1410
|
-
)
|
|
1411
|
-
|
|
1412
|
-
const isActive = isHovered || isFocused
|
|
1413
|
-
|
|
1414
|
-
return (
|
|
1415
|
-
<div
|
|
1416
|
-
className="relative w-full touch-none"
|
|
1417
|
-
onPointerEnter={() => {
|
|
1418
|
-
if (!disabled) setIsHovered(true)
|
|
1419
|
-
}}
|
|
1420
|
-
onPointerLeave={() => {
|
|
1421
|
-
if (!disabled) {
|
|
1422
|
-
setIsHovered(false)
|
|
1423
|
-
setHoverPreview(null)
|
|
1424
|
-
}
|
|
1425
|
-
}}
|
|
1426
|
-
onMouseMove={e => {
|
|
1427
|
-
if (disabled || dragging.current || handleDragging.current) return
|
|
1428
|
-
computeHoverPreview(e.clientX)
|
|
1429
|
-
}}
|
|
1430
|
-
>
|
|
1431
|
-
{/* Extended hit area — 8px beyond each edge */}
|
|
1432
|
-
<div
|
|
1433
|
-
className="absolute cursor-ew-resize"
|
|
1434
|
-
style={{ left: -8, right: -8, top: 0, bottom: 0 }}
|
|
1435
|
-
onPointerDown={handlePointerDown}
|
|
1436
|
-
onPointerMove={handlePointerMove}
|
|
1437
|
-
onPointerUp={handlePointerUp}
|
|
1438
|
-
onPointerCancel={handlePointerUp}
|
|
1439
|
-
/>
|
|
1440
|
-
{/* Hover value tooltip — outside overflow-hidden container */}
|
|
1441
|
-
<AnimatePresence>
|
|
1442
|
-
{hoverPreview && showHoverTooltip && !isPressed && (
|
|
1443
|
-
<motion.div
|
|
1444
|
-
key="hover-tooltip"
|
|
1445
|
-
className="absolute -translate-x-1/2 pointer-events-none z-20"
|
|
1446
|
-
initial={{ opacity: 0, y: 4 }}
|
|
1447
|
-
animate={{ opacity: 1, y: 0 }}
|
|
1448
|
-
exit={{ opacity: 0, y: 4, transition: spring.fast.exit }}
|
|
1449
|
-
transition={spring.fast}
|
|
1450
|
-
style={{
|
|
1451
|
-
left: hoverPreview.cursorX,
|
|
1452
|
-
top: -30,
|
|
1453
|
-
}}
|
|
1454
|
-
>
|
|
1455
|
-
<span
|
|
1456
|
-
className={cn(
|
|
1457
|
-
'text-caption text-background tabular-nums whitespace-nowrap bg-foreground px-2 py-1',
|
|
1458
|
-
shape.bg,
|
|
1459
|
-
'weight-medium',
|
|
1460
|
-
)}
|
|
1461
|
-
>
|
|
1462
|
-
{formatValue(hoverPreview.snappedValue)}
|
|
1463
|
-
</span>
|
|
1464
|
-
</motion.div>
|
|
1465
|
-
)}
|
|
1466
|
-
</AnimatePresence>
|
|
1467
|
-
|
|
1468
|
-
<motion.div
|
|
1469
|
-
ref={mergedRef}
|
|
1470
|
-
className={cn(
|
|
1471
|
-
'relative w-full h-8 select-none touch-none border border-border overflow-hidden outline-offset-2',
|
|
1472
|
-
variant === 'scrubber' ? 'flex items-center gap-3 px-4 cursor-ew-resize' : 'cursor-ew-resize',
|
|
1473
|
-
shape.bg,
|
|
1474
|
-
disabled && 'opacity-50 pointer-events-none',
|
|
1475
|
-
className,
|
|
1476
|
-
)}
|
|
1477
|
-
initial={false}
|
|
1478
|
-
animate={{
|
|
1479
|
-
outline: isFocused ? '1px solid var(--focus-ring)' : '1px solid transparent',
|
|
1480
|
-
}}
|
|
1481
|
-
transition={spring.fast}
|
|
1482
|
-
onPointerDown={handlePointerDown}
|
|
1483
|
-
onPointerMove={handlePointerMove}
|
|
1484
|
-
onPointerUp={handlePointerUp}
|
|
1485
|
-
onPointerCancel={handlePointerUp}
|
|
1486
|
-
{...props}
|
|
1487
|
-
>
|
|
1488
|
-
{/* Invisible Base UI Slider for keyboard nav + a11y */}
|
|
1489
|
-
<SliderPrimitive.Root
|
|
1490
|
-
value={[value]}
|
|
1491
|
-
onValueChange={v => handleRadixChange(v as number[])}
|
|
1492
|
-
onValueCommitted={v => onValueCommitRef.current?.((v as number[])[0]!)}
|
|
1493
|
-
min={min}
|
|
1494
|
-
max={max}
|
|
1495
|
-
step={step}
|
|
1496
|
-
disabled={disabled}
|
|
1497
|
-
className="absolute inset-0 opacity-0 pointer-events-none [&_*]:pointer-events-none"
|
|
1498
|
-
>
|
|
1499
|
-
<SliderPrimitive.Control className="w-full h-full">
|
|
1500
|
-
<SliderPrimitive.Track className="w-full h-full">
|
|
1501
|
-
<SliderPrimitive.Indicator />
|
|
1502
|
-
</SliderPrimitive.Track>
|
|
1503
|
-
<SliderPrimitive.Thumb
|
|
1504
|
-
index={0}
|
|
1505
|
-
aria-label={ariaLabel || label}
|
|
1506
|
-
className="block outline-none"
|
|
1507
|
-
onFocus={e => {
|
|
1508
|
-
if ((e.target as HTMLElement).matches(':focus-visible')) setIsFocused(true)
|
|
1509
|
-
}}
|
|
1510
|
-
onBlur={() => setIsFocused(false)}
|
|
1511
|
-
/>
|
|
1512
|
-
</SliderPrimitive.Control>
|
|
1513
|
-
</SliderPrimitive.Root>
|
|
1514
|
-
|
|
1515
|
-
{/* Hover preview */}
|
|
1516
|
-
<motion.div
|
|
1517
|
-
className="absolute inset-y-0 bg-accent/40 pointer-events-none z-[3]"
|
|
1518
|
-
initial={false}
|
|
1519
|
-
animate={{
|
|
1520
|
-
opacity: hoverPreview && !isPressed ? 1 : 0,
|
|
1521
|
-
}}
|
|
1522
|
-
transition={{ opacity: { duration: 0.15 } }}
|
|
1523
|
-
style={{
|
|
1524
|
-
left: hoverPreview ? hoverPreview.left : 0,
|
|
1525
|
-
width: hoverPreview ? hoverPreview.width : 0,
|
|
1526
|
-
}}
|
|
1527
|
-
/>
|
|
1528
|
-
|
|
1529
|
-
{/* Pips: dots layer — z-[1] */}
|
|
1530
|
-
{variant === 'pips' && (
|
|
1531
|
-
<motion.div
|
|
1532
|
-
className="absolute inset-0 flex justify-between items-center px-3 pointer-events-none z-[1]"
|
|
1533
|
-
style={{ WebkitMaskImage: pipsMaskStyle, maskImage: pipsMaskStyle }}
|
|
1534
|
-
>
|
|
1535
|
-
{pipSteps.map(pipValue => {
|
|
1536
|
-
const isActivePip = pipValue === value
|
|
1537
|
-
return (
|
|
1538
|
-
<div
|
|
1539
|
-
key={pipValue}
|
|
1540
|
-
className="relative flex items-center justify-center"
|
|
1541
|
-
style={{ width: PIP_SIZE, height: PIP_SIZE }}
|
|
1542
|
-
>
|
|
1543
|
-
<motion.div
|
|
1544
|
-
className="rounded-full"
|
|
1545
|
-
initial={false}
|
|
1546
|
-
animate={{
|
|
1547
|
-
backgroundColor: isActivePip ? 'var(--foreground)' : 'var(--muted-foreground)',
|
|
1548
|
-
opacity: isActivePip ? 1 : 0.3,
|
|
1549
|
-
}}
|
|
1550
|
-
transition={spring.fast}
|
|
1551
|
-
style={{ width: PIP_SIZE, height: PIP_SIZE }}
|
|
1552
|
-
/>
|
|
1553
|
-
</div>
|
|
1554
|
-
)
|
|
1555
|
-
})}
|
|
1556
|
-
</motion.div>
|
|
1557
|
-
)}
|
|
1558
|
-
|
|
1559
|
-
{/* Pips: label + value BG layer — z-[2] (occludes dots behind text) */}
|
|
1560
|
-
{variant === 'pips' && (
|
|
1561
|
-
<div className="absolute inset-0 flex items-center px-2 z-[2] pointer-events-none" aria-hidden>
|
|
1562
|
-
{label && <span className="text-body px-2 bg-background text-transparent select-none">{label}</span>}
|
|
1563
|
-
<span
|
|
1564
|
-
className="text-body tabular-nums ml-auto px-2 bg-background text-transparent select-none"
|
|
1565
|
-
style={{ minWidth: `${String(formatValue(max)).length}ch` }}
|
|
1566
|
-
>
|
|
1567
|
-
{formatValue(value)}
|
|
1568
|
-
</span>
|
|
1569
|
-
</div>
|
|
1570
|
-
)}
|
|
1571
|
-
|
|
1572
|
-
{/* Pips: fill — z-[3] */}
|
|
1573
|
-
{variant === 'pips' && (
|
|
1574
|
-
<motion.div
|
|
1575
|
-
className="absolute left-0 top-0 bottom-0 bg-active pointer-events-none z-[3]"
|
|
1576
|
-
style={{
|
|
1577
|
-
width: pipsFillWidthStyle,
|
|
1578
|
-
}}
|
|
1579
|
-
/>
|
|
1580
|
-
)}
|
|
1581
|
-
|
|
1582
|
-
{/* Pips: handle line — z-[3] */}
|
|
1583
|
-
{variant === 'pips' && (
|
|
1584
|
-
<motion.div
|
|
1585
|
-
className="absolute rounded-full pointer-events-none z-[3]"
|
|
1586
|
-
initial={false}
|
|
1587
|
-
animate={{
|
|
1588
|
-
top: isActive ? 7 : 8,
|
|
1589
|
-
bottom: isActive ? 7 : 8,
|
|
1590
|
-
backgroundColor: isFocused
|
|
1591
|
-
? 'var(--foreground)'
|
|
1592
|
-
: isHovered
|
|
1593
|
-
? 'color-mix(in srgb, var(--foreground) 50%, transparent)'
|
|
1594
|
-
: 'color-mix(in srgb, var(--foreground) 25%, transparent)',
|
|
1595
|
-
}}
|
|
1596
|
-
transition={spring.fast}
|
|
1597
|
-
style={{
|
|
1598
|
-
left: pipsHandleLineLeftStyle,
|
|
1599
|
-
width: 2,
|
|
1600
|
-
}}
|
|
1601
|
-
/>
|
|
1602
|
-
)}
|
|
1603
|
-
|
|
1604
|
-
{/* Pips: label + value text layer — z-[4] */}
|
|
1605
|
-
{variant === 'pips' && (
|
|
1606
|
-
<div className="absolute inset-0 flex items-center px-2 z-[4] pointer-events-none">
|
|
1607
|
-
{label && (
|
|
1608
|
-
<motion.span
|
|
1609
|
-
className="text-body px-2"
|
|
1610
|
-
initial={false}
|
|
1611
|
-
animate={{ color: isActive ? 'var(--foreground)' : 'var(--muted-foreground)' }}
|
|
1612
|
-
transition={spring.fast}
|
|
1613
|
-
>
|
|
1614
|
-
{label}
|
|
1615
|
-
</motion.span>
|
|
1616
|
-
)}
|
|
1617
|
-
<motion.span
|
|
1618
|
-
className="text-body tabular-nums ml-auto px-2"
|
|
1619
|
-
initial={false}
|
|
1620
|
-
animate={{ color: isActive ? 'var(--foreground)' : 'var(--muted-foreground)' }}
|
|
1621
|
-
transition={spring.fast}
|
|
1622
|
-
style={{ minWidth: `${String(formatValue(max)).length}ch`, textAlign: 'right' }}
|
|
1623
|
-
>
|
|
1624
|
-
{formatValue(value)}
|
|
1625
|
-
</motion.span>
|
|
1626
|
-
</div>
|
|
1627
|
-
)}
|
|
1628
|
-
|
|
1629
|
-
{/* Scrubber: fill */}
|
|
1630
|
-
{variant === 'scrubber' && (
|
|
1631
|
-
<motion.div
|
|
1632
|
-
className="absolute left-0 top-0 bottom-0 bg-active pointer-events-none"
|
|
1633
|
-
style={{
|
|
1634
|
-
width: fillWidthStyle,
|
|
1635
|
-
}}
|
|
1636
|
-
/>
|
|
1637
|
-
)}
|
|
1638
|
-
|
|
1639
|
-
{/* Scrubber: handle line */}
|
|
1640
|
-
{variant === 'scrubber' && (
|
|
1641
|
-
<motion.div
|
|
1642
|
-
className="absolute rounded-full pointer-events-none z-10"
|
|
1643
|
-
initial={false}
|
|
1644
|
-
animate={{
|
|
1645
|
-
top: isActive ? 7 : 8,
|
|
1646
|
-
bottom: isActive ? 7 : 8,
|
|
1647
|
-
backgroundColor: isFocused
|
|
1648
|
-
? 'var(--foreground)'
|
|
1649
|
-
: isHovered
|
|
1650
|
-
? 'color-mix(in srgb, var(--foreground) 50%, transparent)'
|
|
1651
|
-
: 'color-mix(in srgb, var(--foreground) 25%, transparent)',
|
|
1652
|
-
}}
|
|
1653
|
-
transition={spring.fast}
|
|
1654
|
-
style={{
|
|
1655
|
-
left: handleLineLeftStyle,
|
|
1656
|
-
width: 2,
|
|
1657
|
-
}}
|
|
1658
|
-
/>
|
|
1659
|
-
)}
|
|
1660
|
-
|
|
1661
|
-
{/* Scrubber: label */}
|
|
1662
|
-
{variant === 'scrubber' && label && (
|
|
1663
|
-
<motion.span
|
|
1664
|
-
className="text-body shrink-0 z-10"
|
|
1665
|
-
initial={false}
|
|
1666
|
-
animate={{ color: isActive ? 'var(--foreground)' : 'var(--muted-foreground)' }}
|
|
1667
|
-
transition={spring.fast}
|
|
1668
|
-
>
|
|
1669
|
-
{label}
|
|
1670
|
-
</motion.span>
|
|
1671
|
-
)}
|
|
1672
|
-
|
|
1673
|
-
{/* Scrubber: flex-1 spacer + value */}
|
|
1674
|
-
{variant === 'scrubber' && (
|
|
1675
|
-
<>
|
|
1676
|
-
<div className="flex-1" />
|
|
1677
|
-
<motion.span
|
|
1678
|
-
className="text-body shrink-0 tabular-nums text-right z-10"
|
|
1679
|
-
initial={false}
|
|
1680
|
-
animate={{ color: isActive ? 'var(--foreground)' : 'var(--muted-foreground)' }}
|
|
1681
|
-
transition={spring.fast}
|
|
1682
|
-
style={{ minWidth: `${String(formatValue(max)).length}ch` }}
|
|
1683
|
-
>
|
|
1684
|
-
{formatValue(value)}
|
|
1685
|
-
</motion.span>
|
|
1686
|
-
</>
|
|
1687
|
-
)}
|
|
1688
|
-
|
|
1689
|
-
{/* Resize handle (scrubber only) */}
|
|
1690
|
-
{variant === 'scrubber' && (
|
|
1691
|
-
<motion.div
|
|
1692
|
-
className="absolute top-0 bottom-0 w-2 cursor-ew-resize z-20"
|
|
1693
|
-
style={{ left: handleLeftStyle }}
|
|
1694
|
-
onPointerDown={handleResizePointerDown}
|
|
1695
|
-
onPointerMove={handleResizePointerMove}
|
|
1696
|
-
onPointerUp={handleResizePointerUp}
|
|
1697
|
-
onPointerCancel={handleResizePointerUp}
|
|
1698
|
-
/>
|
|
1699
|
-
)}
|
|
1700
|
-
</motion.div>
|
|
1701
|
-
</div>
|
|
1702
|
-
)
|
|
1703
|
-
},
|
|
1704
|
-
)
|
|
1705
|
-
|
|
1706
|
-
ComfortableSlider.displayName = 'SliderComfortable'
|
|
1707
|
-
|
|
1708
|
-
// ---------------------------------------------------------------------------
|
|
1709
|
-
// Slider — the public component, riding the size ladder.
|
|
1710
|
-
//
|
|
1711
|
-
// The two historical designs are the two ladder steps: the comfortable
|
|
1712
|
-
// pips/scrubber design renders at the default step, the dense original
|
|
1713
|
-
// design at the compact step. Features only the compact engine implements
|
|
1714
|
-
// (range values, discrete step lists, value display, track styling) force
|
|
1715
|
-
// it regardless of the resolved step, so no capability is ever lost.
|
|
1716
|
-
// ---------------------------------------------------------------------------
|
|
1717
|
-
|
|
1718
|
-
interface SliderProps extends Omit<SliderEngineProps, 'value' | 'onChange'> {
|
|
1719
|
-
/** Step on the size ladder (see Sizes). Default renders the pip/scrubber design; compact renders the dense design. Defaults to the surrounding SizeProvider, else `"default"`. */
|
|
1720
|
-
size?: SizeVariant
|
|
1721
|
-
/** Default-step layout. "pips" shows discrete dot indicators; "scrubber" drags anywhere in the row for a continuous value. Ignored when the compact design renders. Defaults to `"pips"`. */
|
|
1722
|
-
variant?: 'pips' | 'scrubber'
|
|
1723
|
-
/** Current value (controlled). Pass an array to enable range mode with two thumbs. Omit it to let the slider keep its own state, seeded from `defaultValue`. */
|
|
1724
|
-
value?: SliderValue
|
|
1725
|
-
/** Initial value when uncontrolled. An array starts the slider in range mode. Defaults to the lowest of `steps`, else `min`, else `0`. */
|
|
1726
|
-
defaultValue?: SliderValue
|
|
1727
|
-
/** Called when the value changes via drag, click, or keyboard. */
|
|
1728
|
-
onChange?: (value: SliderValue) => void
|
|
1729
|
-
/** Minimum value. Defaults to `0`. */
|
|
1730
|
-
min?: number
|
|
1731
|
-
/** Maximum value. Defaults to `100`. */
|
|
1732
|
-
max?: number
|
|
1733
|
-
/** Step increment. Thumb snaps to the nearest step during drag. Defaults to `1`. */
|
|
1734
|
-
step?: number
|
|
1735
|
-
/** Discrete list of allowed values, e.g. 0.1, 0.5, 0.7, 1.1, 1.3. The thumb snaps only to these values and arrow keys walk the list; min/max derive from the list's extremes and step is ignored. */
|
|
1736
|
-
steps?: number[]
|
|
1737
|
-
/** Render dot indicators at each step position on the track. Defaults to `false`. */
|
|
1738
|
-
showSteps?: boolean
|
|
1739
|
-
/** Whether to display the current value label. Defaults to `true`. */
|
|
1740
|
-
showValue?: boolean
|
|
1741
|
-
/** Position of the value label. "tooltip" shows above the thumb during interaction. Defaults to `"left"`. */
|
|
1742
|
-
valuePosition?: ValuePosition
|
|
1743
|
-
/** Custom formatter for the value label. Defaults to `String`. */
|
|
1744
|
-
formatValue?: (v: number) => string
|
|
1745
|
-
/** Accessible label for the slider, also shown as prefix in the value display. */
|
|
1746
|
-
label?: string
|
|
1747
|
-
/** Accessible name of the thumb(s) when it should differ from `label`, e.g. no visible `label` at all. Defaults to `label`. */
|
|
1748
|
-
'aria-label'?: string
|
|
1749
|
-
/** Disables all interaction. Defaults to `false`. */
|
|
1750
|
-
disabled?: boolean
|
|
1751
|
-
/** Layout axis. `"vertical"` stands the track upright (min at the bottom, max at the top; ArrowUp / ArrowRight raise it, ArrowDown / ArrowLeft lower it) and always renders the compact design, which has no vertical pip or scrubber layout. Give it a height with `className` (`h-48`). `valuePosition` then defaults to `"top"`. Defaults to `"horizontal"`. */
|
|
1752
|
-
orientation?: 'horizontal' | 'vertical'
|
|
1753
|
-
/** Called once when an interaction ends with the value it settled on: the pointer lifting after a drag or click, a keyboard step, or a committed value edit. `onChange` reports every value on the way. */
|
|
1754
|
-
onValueCommit?: (value: SliderValue) => void
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
/**
|
|
1758
|
-
* One slider, two ladder steps: the default step's pip/scrubber design, and
|
|
1759
|
-
* the compact step's dense design with track fill, range mode, and value
|
|
1760
|
-
* display.
|
|
1761
|
-
*
|
|
1762
|
-
* One component: the default step renders the pip/scrubber design, the
|
|
1763
|
-
* compact step the dense one. Compact-only props (an array value, steps,
|
|
1764
|
-
* showSteps, showValue, valuePosition, track styling) always render the
|
|
1765
|
-
* compact design so no capability is lost. Either design snaps to the step
|
|
1766
|
-
* grid while dragging, previews the value you are about to pick as a filled
|
|
1767
|
-
* bar plus a tooltip, and carries an invisible Base UI slider underneath for
|
|
1768
|
-
* the ARIA roles and arrow-key navigation. In the dense design the value
|
|
1769
|
-
* label is click-to-edit.
|
|
1770
|
-
*
|
|
1771
|
-
* Works controlled (`value` + `onChange`, exactly as upstream) or
|
|
1772
|
-
* uncontrolled (`defaultValue`, with `onChange` reporting each change).
|
|
1773
|
-
* Track styling props are inherited from the compact engine:
|
|
1774
|
-
* `trackClassName`, `trackStyle`, `fillClassName`, `fillStyle`, `hideFill`,
|
|
1775
|
-
* `thumbColor` and `thumbBorderColor` — each one also forces the dense
|
|
1776
|
-
* design.
|
|
1777
|
-
*
|
|
1778
|
-
* @example {@include ./examples.mdx}
|
|
1779
|
-
*/
|
|
1780
|
-
const Slider = forwardRef<HTMLDivElement, SliderProps>(
|
|
1781
|
-
({ size, variant = 'pips', value: valueProp, defaultValue, onChange: onChangeProp, ...rest }, ref) => {
|
|
1782
|
-
// Local addition: controlled + uncontrolled, the way Base UI components
|
|
1783
|
-
// take it. The engines below stay controlled — they read `props.value`
|
|
1784
|
-
// and call `props.onChange` — so this wrapper owns the state when the
|
|
1785
|
-
// caller doesn't.
|
|
1786
|
-
const [value, onChange] = useControllableState<SliderValue>(
|
|
1787
|
-
valueProp,
|
|
1788
|
-
defaultValue ?? (rest.steps && rest.steps.length > 0 ? Math.min(...rest.steps) : (rest.min ?? 0)),
|
|
1789
|
-
onChangeProp,
|
|
1790
|
-
)
|
|
1791
|
-
const props: SliderEngineProps = { ...rest, value, onChange }
|
|
1792
|
-
const resolved = useSizeVariant(size)
|
|
1793
|
-
const needsCompactEngine =
|
|
1794
|
-
Array.isArray(props.value) ||
|
|
1795
|
-
props.steps !== undefined ||
|
|
1796
|
-
props.showSteps !== undefined ||
|
|
1797
|
-
props.showValue !== undefined ||
|
|
1798
|
-
props.valuePosition !== undefined ||
|
|
1799
|
-
props.trackClassName !== undefined ||
|
|
1800
|
-
props.trackStyle !== undefined ||
|
|
1801
|
-
props.fillClassName !== undefined ||
|
|
1802
|
-
props.fillStyle !== undefined ||
|
|
1803
|
-
props.hideFill !== undefined ||
|
|
1804
|
-
props.thumbColor !== undefined ||
|
|
1805
|
-
props.thumbBorderColor !== undefined ||
|
|
1806
|
-
props.orientation === 'vertical'
|
|
1807
|
-
|
|
1808
|
-
if (resolved === 'compact' || needsCompactEngine) {
|
|
1809
|
-
return <CompactSlider ref={ref} {...props} />
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
const {
|
|
1813
|
-
value: singleValue,
|
|
1814
|
-
onChange: singleOnChange,
|
|
1815
|
-
min,
|
|
1816
|
-
max,
|
|
1817
|
-
step,
|
|
1818
|
-
label,
|
|
1819
|
-
formatValue,
|
|
1820
|
-
disabled,
|
|
1821
|
-
onValueCommit,
|
|
1822
|
-
// Compact-engine-only fields — all undefined on this path (any defined
|
|
1823
|
-
// one would have routed to the compact engine above).
|
|
1824
|
-
steps: _steps,
|
|
1825
|
-
showSteps: _showSteps,
|
|
1826
|
-
showValue: _showValue,
|
|
1827
|
-
valuePosition: _valuePosition,
|
|
1828
|
-
trackClassName: _trackClassName,
|
|
1829
|
-
trackStyle: _trackStyle,
|
|
1830
|
-
fillClassName: _fillClassName,
|
|
1831
|
-
fillStyle: _fillStyle,
|
|
1832
|
-
hideFill: _hideFill,
|
|
1833
|
-
thumbColor: _thumbColor,
|
|
1834
|
-
thumbBorderColor: _thumbBorderColor,
|
|
1835
|
-
orientation: _orientation,
|
|
1836
|
-
...html
|
|
1837
|
-
} = props
|
|
1838
|
-
|
|
1839
|
-
return (
|
|
1840
|
-
<ComfortableSlider
|
|
1841
|
-
ref={ref}
|
|
1842
|
-
value={singleValue as number}
|
|
1843
|
-
onChange={singleOnChange as (v: number) => void}
|
|
1844
|
-
min={min}
|
|
1845
|
-
max={max}
|
|
1846
|
-
step={step}
|
|
1847
|
-
variant={variant}
|
|
1848
|
-
label={label}
|
|
1849
|
-
formatValue={formatValue}
|
|
1850
|
-
disabled={disabled}
|
|
1851
|
-
onValueCommit={onValueCommit as ((v: number) => void) | undefined}
|
|
1852
|
-
{...(html as Omit<SliderComfortableProps, 'value' | 'onChange'>)}
|
|
1853
|
-
/>
|
|
1854
|
-
)
|
|
1855
|
-
},
|
|
1856
|
-
)
|
|
1857
|
-
|
|
1858
|
-
Slider.displayName = 'Slider'
|
|
1859
|
-
|
|
1860
|
-
/** @deprecated The comfortable design is now the ladder's default step —
|
|
1861
|
-
* render <Slider> (optionally with `variant`) instead. */
|
|
1862
|
-
const SliderComfortable = ComfortableSlider
|
|
1863
|
-
|
|
1864
|
-
export type { SliderComfortableProps, SliderProps, SliderValue, ValuePosition }
|
|
1865
|
-
export { Slider, SliderComfortable }
|
|
1866
|
-
|
|
1867
|
-
export default Slider
|