@bug-on/md3-react 2.0.3 → 3.0.1
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/.turbo/turbo-build.log +42 -0
- package/CHANGELOG.md +69 -0
- package/dist/index.css +178 -0
- package/dist/index.css.d.ts +2 -0
- package/dist/index.d.mts +6135 -0
- package/dist/index.d.ts +6135 -71
- package/dist/index.js +1688 -631
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1600 -564
- package/dist/index.mjs.map +1 -1
- package/dist/material-symbols-cdn.css.d.ts +2 -0
- package/dist/material-symbols-self-hosted.css.d.ts +2 -0
- package/dist/plugin.d.mts +1 -0
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.js +13 -0
- package/dist/plugin.js.map +1 -0
- package/dist/plugin.mjs +3 -0
- package/dist/plugin.mjs.map +1 -0
- package/dist/typography.css.d.ts +2 -0
- package/package.json +28 -19
- package/scripts/copy-assets.js +115 -0
- package/src/assets/fonts/GoogleSansFlex-VariableFont.woff2 +0 -0
- package/src/assets/fonts/MaterialSymbolsOutlined-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
- package/src/assets/fonts/MaterialSymbolsRounded-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
- package/src/assets/fonts/MaterialSymbolsSharp-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
- package/src/assets/loading-indicator.svg +19 -0
- package/src/assets/material-symbols-cdn.css +65 -0
- package/src/assets/material-symbols-self-hosted.css +90 -0
- package/src/css.d.ts +20 -0
- package/src/hooks/useClickOutside.ts +37 -0
- package/src/hooks/useMediaQuery.ts +28 -0
- package/src/hooks/useRipple.ts +88 -0
- package/src/index.css +23 -0
- package/src/index.ts +349 -0
- package/src/lib/material-symbols-preconnect.tsx +82 -0
- package/src/lib/theme-utils.ts +195 -0
- package/src/lib/utils.ts +6 -0
- package/src/plugin.ts +12 -0
- package/src/test/button.test.tsx +59 -0
- package/src/test/icon.test.tsx +91 -0
- package/src/test/loading-indicator.test.tsx +128 -0
- package/src/test/progress-indicator.test.tsx +306 -0
- package/src/test/setup.ts +80 -0
- package/src/test/typography.test.tsx +206 -0
- package/src/types/index.ts +7 -0
- package/src/types/md3.ts +31 -0
- package/src/ui/Text.tsx +60 -0
- package/src/ui/__snapshots__/divider.test.tsx.snap +63 -0
- package/src/ui/app-bar/app-bar-column.tsx +99 -0
- package/src/ui/app-bar/app-bar-item-button.tsx +71 -0
- package/src/ui/app-bar/app-bar-items.test.tsx +89 -0
- package/src/ui/app-bar/app-bar-overflow-indicator.tsx +108 -0
- package/src/ui/app-bar/app-bar-row.tsx +104 -0
- package/src/ui/app-bar/app-bar.test.tsx +87 -0
- package/src/ui/app-bar/app-bar.tokens.ts +223 -0
- package/src/ui/app-bar/app-bar.types.ts +441 -0
- package/src/ui/app-bar/bottom-app-bar.test.tsx +42 -0
- package/src/ui/app-bar/bottom-app-bar.tsx +84 -0
- package/src/ui/app-bar/docked-toolbar.test.tsx +34 -0
- package/src/ui/app-bar/docked-toolbar.tsx +54 -0
- package/src/ui/app-bar/flexible-app-bar.test.tsx +75 -0
- package/src/ui/app-bar/hooks/use-app-bar-scroll.ts +110 -0
- package/src/ui/app-bar/hooks/use-flexible-app-bar.ts +123 -0
- package/{dist/ui/app-bar/index.d.ts → src/ui/app-bar/index.ts} +35 -2
- package/src/ui/app-bar/large-flexible-app-bar.tsx +165 -0
- package/src/ui/app-bar/medium-flexible-app-bar.tsx +167 -0
- package/src/ui/app-bar/search-app-bar.test.tsx +49 -0
- package/src/ui/app-bar/search-app-bar.tsx +176 -0
- package/src/ui/app-bar/search-view.tsx +227 -0
- package/src/ui/app-bar/small-app-bar.test.tsx +48 -0
- package/src/ui/app-bar/small-app-bar.tsx +203 -0
- package/src/ui/badge.test.tsx +345 -0
- package/src/ui/badge.tsx +282 -0
- package/src/ui/button-group.test.tsx +71 -0
- package/src/ui/button-group.tsx +350 -0
- package/src/ui/button.test.tsx +306 -0
- package/src/ui/button.tsx +665 -0
- package/src/ui/card.test.tsx +187 -0
- package/src/ui/card.tsx +259 -0
- package/src/ui/checkbox.test.tsx +423 -0
- package/src/ui/checkbox.tsx +525 -0
- package/src/ui/chip.test.tsx +292 -0
- package/src/ui/chip.tsx +548 -0
- package/src/ui/code-block.tsx +219 -0
- package/src/ui/dialog.test.tsx +300 -0
- package/src/ui/dialog.tsx +384 -0
- package/src/ui/divider.test.tsx +314 -0
- package/src/ui/divider.tsx +412 -0
- package/src/ui/drawer.tsx +240 -0
- package/src/ui/fab-menu.test.tsx +494 -0
- package/src/ui/fab-menu.tsx +739 -0
- package/src/ui/fab.test.tsx +232 -0
- package/src/ui/fab.tsx +505 -0
- package/src/ui/icon-button.test.tsx +515 -0
- package/src/ui/icon-button.tsx +525 -0
- package/src/ui/icon.test.tsx +197 -0
- package/src/ui/icon.tsx +179 -0
- package/src/ui/loading-indicator.test.tsx +73 -0
- package/src/ui/loading-indicator.tsx +312 -0
- package/src/ui/menu/context-menu.tsx +275 -0
- package/src/ui/menu/index.ts +77 -0
- package/src/ui/menu/menu-animations.ts +102 -0
- package/src/ui/menu/menu-context.tsx +99 -0
- package/src/ui/menu/menu-divider.tsx +47 -0
- package/src/ui/menu/menu-group.tsx +200 -0
- package/src/ui/menu/menu-item.tsx +294 -0
- package/src/ui/menu/menu-tokens.ts +208 -0
- package/src/ui/menu/menu-types.ts +313 -0
- package/src/ui/menu/menu.test.tsx +624 -0
- package/src/ui/menu/menu.tsx +289 -0
- package/src/ui/menu/sub-menu.tsx +223 -0
- package/src/ui/menu/vertical-menu.tsx +382 -0
- package/src/ui/navigation-rail.test.tsx +404 -0
- package/src/ui/navigation-rail.tsx +607 -0
- package/src/ui/progress-indicator/circular.tsx +248 -0
- package/src/ui/progress-indicator/hooks.ts +51 -0
- package/{dist/ui/progress-indicator/index.d.ts → src/ui/progress-indicator/index.tsx} +20 -2
- package/src/ui/progress-indicator/linear-flat.tsx +83 -0
- package/src/ui/progress-indicator/linear-wavy.tsx +243 -0
- package/src/ui/progress-indicator/linear.tsx +143 -0
- package/src/ui/progress-indicator/types.ts +158 -0
- package/src/ui/progress-indicator/utils.ts +73 -0
- package/src/ui/radio-button.test.tsx +407 -0
- package/src/ui/radio-button.tsx +551 -0
- package/src/ui/ripple.test.tsx +72 -0
- package/src/ui/ripple.tsx +234 -0
- package/src/ui/scroll-area.test.tsx +58 -0
- package/src/ui/scroll-area.tsx +139 -0
- package/src/ui/search/animated-placeholder.tsx +145 -0
- package/src/ui/search/hooks/use-search-keyboard.test.ts +202 -0
- package/src/ui/search/hooks/use-search-keyboard.ts +104 -0
- package/src/ui/search/hooks/use-search-view-focus.test.ts +96 -0
- package/src/ui/search/hooks/use-search-view-focus.ts +24 -0
- package/src/ui/search/index.ts +44 -0
- package/src/ui/search/search-bar.tsx +220 -0
- package/src/ui/search/search-context.tsx +42 -0
- package/src/ui/search/search-view-docked.tsx +194 -0
- package/src/ui/search/search-view-fullscreen.tsx +247 -0
- package/src/ui/search/search.test.tsx +233 -0
- package/src/ui/search/search.tokens.ts +134 -0
- package/src/ui/search/search.tsx +131 -0
- package/src/ui/search/search.types.ts +154 -0
- package/src/ui/search/trailing-action.tsx +49 -0
- package/src/ui/shared/constants.ts +135 -0
- package/{dist/ui/shared/touch-target.d.ts → src/ui/shared/touch-target.tsx} +13 -1
- package/src/ui/slider/hooks/useSliderMath.ts +195 -0
- package/{dist/ui/slider/index.d.ts → src/ui/slider/index.ts} +12 -1
- package/src/ui/slider/range-slider.tsx +561 -0
- package/src/ui/slider/slider-thumb.tsx +379 -0
- package/src/ui/slider/slider-track.tsx +912 -0
- package/src/ui/slider/slider.tokens.ts +189 -0
- package/src/ui/slider/slider.tsx +259 -0
- package/src/ui/slider/slider.types.ts +288 -0
- package/src/ui/snackbar/index.ts +20 -0
- package/src/ui/snackbar/snackbar.test.tsx +338 -0
- package/src/ui/snackbar/snackbar.tsx +476 -0
- package/{dist/ui/switch/index.d.ts → src/ui/switch/index.ts} +1 -0
- package/src/ui/switch/switch.stories.tsx +309 -0
- package/src/ui/switch/switch.test.tsx +243 -0
- package/src/ui/switch/switch.tokens.ts +89 -0
- package/src/ui/switch/switch.tsx +504 -0
- package/src/ui/switch/switch.types.ts +62 -0
- package/{dist/ui/tabs/index.d.ts → src/ui/tabs/index.ts} +8 -1
- package/src/ui/tabs/tab.tsx +407 -0
- package/src/ui/tabs/tabs-content.tsx +89 -0
- package/src/ui/tabs/tabs-list.tsx +146 -0
- package/src/ui/tabs/tabs.test.tsx +290 -0
- package/src/ui/tabs/tabs.tokens.ts +121 -0
- package/src/ui/tabs/tabs.tsx +229 -0
- package/src/ui/tabs/tabs.types.ts +185 -0
- package/{dist/ui/text-field/index.d.ts → src/ui/text-field/index.ts} +8 -1
- package/src/ui/text-field/subcomponents/active-indicator.tsx +67 -0
- package/src/ui/text-field/subcomponents/floating-label.tsx +161 -0
- package/src/ui/text-field/subcomponents/leading-icon.tsx +46 -0
- package/src/ui/text-field/subcomponents/outline-container.tsx +170 -0
- package/src/ui/text-field/subcomponents/prefix-suffix.tsx +59 -0
- package/src/ui/text-field/subcomponents/supporting-text.tsx +145 -0
- package/src/ui/text-field/subcomponents/trailing-icon.tsx +199 -0
- package/src/ui/text-field/text-field.test.tsx +454 -0
- package/src/ui/text-field/text-field.tokens.ts +104 -0
- package/src/ui/text-field/text-field.tsx +548 -0
- package/src/ui/text-field/text-field.types.ts +180 -0
- package/src/ui/theme-provider/index.tsx +215 -0
- package/src/ui/toc.test.tsx +108 -0
- package/src/ui/toc.tsx +172 -0
- package/src/ui/tooltip/plain-tooltip.tsx +63 -0
- package/src/ui/tooltip/rich-tooltip.tsx +94 -0
- package/src/ui/tooltip/tooltip-box.tsx +266 -0
- package/src/ui/tooltip/tooltip-caret-shape.tsx +68 -0
- package/src/ui/tooltip/tooltip.tokens.ts +26 -0
- package/src/ui/tooltip/tooltip.types.ts +70 -0
- package/src/ui/tooltip/use-tooltip-position.ts +208 -0
- package/src/ui/tooltip/use-tooltip-state.ts +41 -0
- package/src/ui/typography/__tests__/typography.test.tsx +170 -0
- package/{dist/ui/typography/index.d.ts → src/ui/typography/index.ts} +21 -3
- package/src/ui/typography/type-scale-tokens.ts +205 -0
- package/src/ui/typography/typography-key-tokens.ts +43 -0
- package/src/ui/typography/typography-tokens.ts +360 -0
- package/src/ui/typography/typography.css +22 -0
- package/src/ui/typography/typography.tsx +559 -0
- package/test-render.tsx +4 -0
- package/test-shadow.html +26 -0
- package/test_output.txt +164 -0
- package/test_output_v2.txt +5 -0
- package/tsconfig.build.json +10 -0
- package/tsconfig.json +18 -0
- package/tsup.config.ts +20 -0
- package/vitest.config.ts +11 -0
- package/dist/hooks/useClickOutside.d.ts +0 -8
- package/dist/hooks/useMediaQuery.d.ts +0 -11
- package/dist/hooks/useRipple.d.ts +0 -26
- package/dist/lib/material-symbols-preconnect.d.ts +0 -42
- package/dist/lib/theme-utils.d.ts +0 -63
- package/dist/lib/utils.d.ts +0 -2
- package/dist/types/index.d.ts +0 -1
- package/dist/types/md3.d.ts +0 -14
- package/dist/ui/app-bar/app-bar-column.d.ts +0 -28
- package/dist/ui/app-bar/app-bar-item-button.d.ts +0 -16
- package/dist/ui/app-bar/app-bar-overflow-indicator.d.ts +0 -18
- package/dist/ui/app-bar/app-bar-row.d.ts +0 -36
- package/dist/ui/app-bar/app-bar.tokens.d.ts +0 -184
- package/dist/ui/app-bar/app-bar.types.d.ts +0 -392
- package/dist/ui/app-bar/bottom-app-bar.d.ts +0 -31
- package/dist/ui/app-bar/docked-toolbar.d.ts +0 -25
- package/dist/ui/app-bar/hooks/use-app-bar-scroll.d.ts +0 -42
- package/dist/ui/app-bar/hooks/use-flexible-app-bar.d.ts +0 -37
- package/dist/ui/app-bar/large-flexible-app-bar.d.ts +0 -26
- package/dist/ui/app-bar/medium-flexible-app-bar.d.ts +0 -28
- package/dist/ui/app-bar/search-app-bar.d.ts +0 -43
- package/dist/ui/app-bar/search-view.d.ts +0 -54
- package/dist/ui/app-bar/small-app-bar.d.ts +0 -37
- package/dist/ui/badge.d.ts +0 -125
- package/dist/ui/button-group.d.ts +0 -59
- package/dist/ui/button.d.ts +0 -148
- package/dist/ui/card.d.ts +0 -62
- package/dist/ui/checkbox.d.ts +0 -82
- package/dist/ui/chip.d.ts +0 -110
- package/dist/ui/code-block.d.ts +0 -14
- package/dist/ui/dialog.d.ts +0 -111
- package/dist/ui/divider.d.ts +0 -164
- package/dist/ui/drawer.d.ts +0 -39
- package/dist/ui/dropdown.d.ts +0 -29
- package/dist/ui/fab-menu.d.ts +0 -204
- package/dist/ui/fab.d.ts +0 -162
- package/dist/ui/icon-button.d.ts +0 -131
- package/dist/ui/icon.d.ts +0 -88
- package/dist/ui/loading-indicator.d.ts +0 -42
- package/dist/ui/navigation-rail.d.ts +0 -29
- package/dist/ui/progress-indicator/circular.d.ts +0 -3
- package/dist/ui/progress-indicator/hooks.d.ts +0 -3
- package/dist/ui/progress-indicator/linear-flat.d.ts +0 -10
- package/dist/ui/progress-indicator/linear-wavy.d.ts +0 -18
- package/dist/ui/progress-indicator/linear.d.ts +0 -3
- package/dist/ui/progress-indicator/types.d.ts +0 -151
- package/dist/ui/progress-indicator/utils.d.ts +0 -3
- package/dist/ui/radio-button.d.ts +0 -106
- package/dist/ui/ripple.d.ts +0 -126
- package/dist/ui/scroll-area.d.ts +0 -27
- package/dist/ui/search/animated-placeholder.d.ts +0 -54
- package/dist/ui/search/hooks/use-search-keyboard.d.ts +0 -32
- package/dist/ui/search/hooks/use-search-view-focus.d.ts +0 -6
- package/dist/ui/search/index.d.ts +0 -27
- package/dist/ui/search/search-bar.d.ts +0 -32
- package/dist/ui/search/search-context.d.ts +0 -24
- package/dist/ui/search/search-view-docked.d.ts +0 -25
- package/dist/ui/search/search-view-fullscreen.d.ts +0 -36
- package/dist/ui/search/search.d.ts +0 -50
- package/dist/ui/search/search.tokens.d.ts +0 -112
- package/dist/ui/search/search.types.d.ts +0 -131
- package/dist/ui/search/trailing-action.d.ts +0 -9
- package/dist/ui/shared/constants.d.ts +0 -86
- package/dist/ui/slider/hooks/useSliderMath.d.ts +0 -101
- package/dist/ui/slider/range-slider.d.ts +0 -47
- package/dist/ui/slider/slider-thumb.d.ts +0 -33
- package/dist/ui/slider/slider-track.d.ts +0 -25
- package/dist/ui/slider/slider.d.ts +0 -60
- package/dist/ui/slider/slider.tokens.d.ts +0 -151
- package/dist/ui/slider/slider.types.d.ts +0 -259
- package/dist/ui/snackbar/index.d.ts +0 -6
- package/dist/ui/snackbar/snackbar.d.ts +0 -197
- package/dist/ui/switch/switch.d.ts +0 -30
- package/dist/ui/switch/switch.stories.d.ts +0 -48
- package/dist/ui/switch/switch.tokens.d.ts +0 -67
- package/dist/ui/switch/switch.types.d.ts +0 -59
- package/dist/ui/tabs/tab.d.ts +0 -43
- package/dist/ui/tabs/tabs-content.d.ts +0 -36
- package/dist/ui/tabs/tabs-list.d.ts +0 -40
- package/dist/ui/tabs/tabs.d.ts +0 -60
- package/dist/ui/tabs/tabs.tokens.d.ts +0 -94
- package/dist/ui/tabs/tabs.types.d.ts +0 -172
- package/dist/ui/text-field/subcomponents/active-indicator.d.ts +0 -24
- package/dist/ui/text-field/subcomponents/floating-label.d.ts +0 -43
- package/dist/ui/text-field/subcomponents/leading-icon.d.ts +0 -23
- package/dist/ui/text-field/subcomponents/outline-container.d.ts +0 -42
- package/dist/ui/text-field/subcomponents/prefix-suffix.d.ts +0 -24
- package/dist/ui/text-field/subcomponents/supporting-text.d.ts +0 -37
- package/dist/ui/text-field/subcomponents/trailing-icon.d.ts +0 -41
- package/dist/ui/text-field/text-field.d.ts +0 -49
- package/dist/ui/text-field/text-field.tokens.d.ts +0 -76
- package/dist/ui/text-field/text-field.types.d.ts +0 -126
- package/dist/ui/theme-provider/index.d.ts +0 -48
- package/dist/ui/toc.d.ts +0 -80
- package/dist/ui/tooltip/plain-tooltip.d.ts +0 -2
- package/dist/ui/tooltip/rich-tooltip.d.ts +0 -2
- package/dist/ui/tooltip/tooltip-box.d.ts +0 -2
- package/dist/ui/tooltip/tooltip-caret-shape.d.ts +0 -9
- package/dist/ui/tooltip/tooltip.tokens.d.ts +0 -26
- package/dist/ui/tooltip/tooltip.types.d.ts +0 -56
- package/dist/ui/tooltip/use-tooltip-position.d.ts +0 -8
- package/dist/ui/tooltip/use-tooltip-state.d.ts +0 -2
- package/dist/ui/typography/type-scale-tokens.d.ts +0 -162
- package/dist/ui/typography/typography-key-tokens.d.ts +0 -40
- package/dist/ui/typography/typography-tokens.d.ts +0 -220
- package/dist/ui/typography/typography.d.ts +0 -265
- /package/{dist/hooks/index.d.ts → src/hooks/index.ts} +0 -0
- /package/{dist/ui/tooltip/index.d.ts → src/ui/tooltip/index.ts} +0 -0
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file slider-track.tsx
|
|
3
|
+
* MD3 Expressive Slider — Track with asymmetric corner radii, 6px thumb gaps,
|
|
4
|
+
* discrete tick marks, and centered-mode support.
|
|
5
|
+
*
|
|
6
|
+
* Design decisions:
|
|
7
|
+
* 1. GAP MATH: The 6px gap between track and thumb is calculated mathematically
|
|
8
|
+
* using CSS calc() — NOT using margin/padding which would break layout.
|
|
9
|
+
* 2. ASYMMETRIC RADII: Inner corners (facing thumb) = 2px; outer ends = size/2 (pill cap).
|
|
10
|
+
* 3. CENTERED MODE: Active segment spans from 50% outward to thumb, not from min.
|
|
11
|
+
* 4. TICKS: 4×4px dots positioned absolutely along track center axis.
|
|
12
|
+
* Color differs on active vs inactive portions of the track.
|
|
13
|
+
* 5. VERTICAL: Uses height/top instead of width/left, with inverted axis
|
|
14
|
+
* (bottom=0%, top=100%) per MD3 vertical spec.
|
|
15
|
+
*
|
|
16
|
+
* @see docs/m3/sliders/Slider.kt#SliderDefaults.Track
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { AnimatePresence, m, useReducedMotion } from "motion/react";
|
|
20
|
+
import * as React from "react";
|
|
21
|
+
import { cn } from "../../lib/utils";
|
|
22
|
+
import { SliderColors, SliderTokens } from "./slider.tokens";
|
|
23
|
+
import type {
|
|
24
|
+
SliderOrientation,
|
|
25
|
+
SliderTrackProps,
|
|
26
|
+
SliderTrackSize,
|
|
27
|
+
SliderVariant,
|
|
28
|
+
} from "./slider.types";
|
|
29
|
+
|
|
30
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Returns border-radius for a horizontal track segment.
|
|
34
|
+
* @param isLeading - whether this segment is on the leading (left) side of the thumb
|
|
35
|
+
*/
|
|
36
|
+
function getHorizontalRadius(
|
|
37
|
+
isLeading: boolean,
|
|
38
|
+
innerR: number,
|
|
39
|
+
outerR: number,
|
|
40
|
+
): React.CSSProperties {
|
|
41
|
+
if (isLeading) {
|
|
42
|
+
return {
|
|
43
|
+
borderTopLeftRadius: outerR,
|
|
44
|
+
borderBottomLeftRadius: outerR,
|
|
45
|
+
borderTopRightRadius: innerR,
|
|
46
|
+
borderBottomRightRadius: innerR,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
borderTopLeftRadius: innerR,
|
|
51
|
+
borderBottomLeftRadius: innerR,
|
|
52
|
+
borderTopRightRadius: outerR,
|
|
53
|
+
borderBottomRightRadius: outerR,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Border-radius for a vertical track segment. */
|
|
58
|
+
function getVerticalRadius(
|
|
59
|
+
isLeading: boolean,
|
|
60
|
+
innerR: number,
|
|
61
|
+
outerR: number,
|
|
62
|
+
): React.CSSProperties {
|
|
63
|
+
if (isLeading) {
|
|
64
|
+
return {
|
|
65
|
+
borderBottomLeftRadius: outerR,
|
|
66
|
+
borderBottomRightRadius: outerR,
|
|
67
|
+
borderTopLeftRadius: innerR,
|
|
68
|
+
borderTopRightRadius: innerR,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
borderTopLeftRadius: outerR,
|
|
73
|
+
borderTopRightRadius: outerR,
|
|
74
|
+
borderBottomLeftRadius: innerR,
|
|
75
|
+
borderBottomRightRadius: innerR,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** All-inner-radius shorthand for centered active segments (no pill caps). */
|
|
80
|
+
const allInnerRadius = (innerR: number): React.CSSProperties => ({
|
|
81
|
+
borderTopLeftRadius: innerR,
|
|
82
|
+
borderBottomLeftRadius: innerR,
|
|
83
|
+
borderTopRightRadius: innerR,
|
|
84
|
+
borderBottomRightRadius: innerR,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ─── Inset Icon ───────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
interface InsetIconProps {
|
|
90
|
+
/** The icon node to render. */
|
|
91
|
+
icon: React.ReactNode;
|
|
92
|
+
/** Whether the icon is currently positioned on the active (filled) segment. */
|
|
93
|
+
isOnActiveSegment: boolean;
|
|
94
|
+
/** The computed 'left' (horizontal) or 'bottom' (vertical) CSS value. */
|
|
95
|
+
position: string;
|
|
96
|
+
orientation: SliderOrientation;
|
|
97
|
+
/** Physical track height/width in px. */
|
|
98
|
+
trackSize: number;
|
|
99
|
+
/** Size token for looking up tokens (e.g. 'xl') */
|
|
100
|
+
trackSizeToken: SliderTrackSize;
|
|
101
|
+
disabled: boolean;
|
|
102
|
+
/** Color variant to match leading/trailing track. */
|
|
103
|
+
variant: SliderVariant;
|
|
104
|
+
/** Suppresses motion when user prefers reduced motion. */
|
|
105
|
+
prefersReduced: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Icon rendered inside the slider track.
|
|
110
|
+
*
|
|
111
|
+
* When the key changes (active ↔ inactive swap), AnimatePresence unmounts this
|
|
112
|
+
* with a fade-out, then mounts the new instance with a fade-in at its target
|
|
113
|
+
* position — producing the MD3 "hop" effect without any position cross-over.
|
|
114
|
+
* When the key stays the same (e.g. trailing-inactive tracking the thumb),
|
|
115
|
+
* the spring on `left`/`bottom` keeps it smoothly glued to the thumb.
|
|
116
|
+
*/
|
|
117
|
+
const InsetIcon = React.memo(function InsetIcon({
|
|
118
|
+
icon,
|
|
119
|
+
isOnActiveSegment,
|
|
120
|
+
position,
|
|
121
|
+
orientation,
|
|
122
|
+
trackSize,
|
|
123
|
+
trackSizeToken,
|
|
124
|
+
disabled,
|
|
125
|
+
variant,
|
|
126
|
+
prefersReduced,
|
|
127
|
+
}: InsetIconProps) {
|
|
128
|
+
const iconSize = Math.min(
|
|
129
|
+
SliderTokens.insetIconSizes[trackSizeToken],
|
|
130
|
+
Math.max(4, trackSize - SliderTokens.insetIconPadding * 2),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const activeColor = `var(--md-sys-color-on-${variant})`;
|
|
134
|
+
const inactiveColor = `var(--md-sys-color-${variant})`;
|
|
135
|
+
const isHorizontal = orientation === "horizontal";
|
|
136
|
+
const fastFade = prefersReduced ? { duration: 0 } : { duration: 0.12 };
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<m.div
|
|
140
|
+
aria-hidden="true"
|
|
141
|
+
className="[&_svg]:w-full [&_svg]:h-full"
|
|
142
|
+
initial={{
|
|
143
|
+
opacity: 0,
|
|
144
|
+
...(isHorizontal ? { left: position } : { bottom: position }),
|
|
145
|
+
}}
|
|
146
|
+
animate={{
|
|
147
|
+
[isHorizontal ? "left" : "bottom"]: position,
|
|
148
|
+
opacity: disabled ? 0.38 : 1,
|
|
149
|
+
color: isOnActiveSegment ? activeColor : inactiveColor,
|
|
150
|
+
}}
|
|
151
|
+
exit={{ opacity: 0, transition: fastFade }}
|
|
152
|
+
transition={
|
|
153
|
+
prefersReduced
|
|
154
|
+
? { duration: 0 }
|
|
155
|
+
: {
|
|
156
|
+
left: { type: "spring", stiffness: 500, damping: 40 },
|
|
157
|
+
bottom: { type: "spring", stiffness: 500, damping: 40 },
|
|
158
|
+
opacity: fastFade,
|
|
159
|
+
color: fastFade,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
style={{
|
|
163
|
+
position: "absolute",
|
|
164
|
+
width: iconSize,
|
|
165
|
+
height: iconSize,
|
|
166
|
+
display: "flex",
|
|
167
|
+
alignItems: "center",
|
|
168
|
+
justifyContent: "center",
|
|
169
|
+
pointerEvents: "none",
|
|
170
|
+
zIndex: 1,
|
|
171
|
+
willChange: isHorizontal ? "left" : "bottom",
|
|
172
|
+
...(isHorizontal
|
|
173
|
+
? { top: "50%", transform: "translateY(-50%)" }
|
|
174
|
+
: { left: "50%", transform: "translateX(-50%)" }),
|
|
175
|
+
}}
|
|
176
|
+
>
|
|
177
|
+
{icon}
|
|
178
|
+
</m.div>
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ─── Tick Marks ───────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
interface TicksProps {
|
|
185
|
+
ticks: number[];
|
|
186
|
+
min: number;
|
|
187
|
+
max: number;
|
|
188
|
+
percent: number;
|
|
189
|
+
orientation: "horizontal" | "vertical";
|
|
190
|
+
variant: SliderVariant;
|
|
191
|
+
disabled: boolean;
|
|
192
|
+
/** Pre-computed track inset — avoids recalculating in every tick render. */
|
|
193
|
+
trackInset: number;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Renders tick dot markers for discrete slider mode. */
|
|
197
|
+
function Ticks({
|
|
198
|
+
ticks,
|
|
199
|
+
min,
|
|
200
|
+
max,
|
|
201
|
+
percent,
|
|
202
|
+
orientation,
|
|
203
|
+
variant,
|
|
204
|
+
isCentered,
|
|
205
|
+
disabled,
|
|
206
|
+
trackInset,
|
|
207
|
+
}: TicksProps & { isCentered?: boolean }) {
|
|
208
|
+
if (ticks.length === 0) return null;
|
|
209
|
+
const { thumbGap, thumbWidthDefault, tickSize } = SliderTokens;
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<>
|
|
213
|
+
{ticks.map((tick) => {
|
|
214
|
+
const tickPercent = (tick - min) / (max - min);
|
|
215
|
+
const isOnActive = isCentered
|
|
216
|
+
? percent >= 0.5
|
|
217
|
+
? tickPercent >= 0.5 && tickPercent <= percent
|
|
218
|
+
: tickPercent <= 0.5 && tickPercent >= percent
|
|
219
|
+
: tickPercent <= percent;
|
|
220
|
+
|
|
221
|
+
// Skip ticks that would be visually hidden inside the thumb gap
|
|
222
|
+
const thumbStart = percent - (thumbGap + thumbWidthDefault / 2) / 100;
|
|
223
|
+
const thumbEnd = percent + (thumbGap + thumbWidthDefault / 2) / 100;
|
|
224
|
+
if (tickPercent > thumbStart && tickPercent < thumbEnd) return null;
|
|
225
|
+
|
|
226
|
+
const color = disabled
|
|
227
|
+
? SliderColors.disabledTick
|
|
228
|
+
: isOnActive
|
|
229
|
+
? `var(--md-sys-color-${variant}-container)`
|
|
230
|
+
: `var(--md-sys-color-${variant})`;
|
|
231
|
+
|
|
232
|
+
const style: React.CSSProperties = {
|
|
233
|
+
position: "absolute",
|
|
234
|
+
width: tickSize,
|
|
235
|
+
height: tickSize,
|
|
236
|
+
borderRadius: "50%",
|
|
237
|
+
backgroundColor: color,
|
|
238
|
+
opacity: disabled ? 0.38 : 1,
|
|
239
|
+
...(orientation === "horizontal"
|
|
240
|
+
? {
|
|
241
|
+
left: `calc(${trackInset}px + ${tickPercent} * (100% - ${trackInset * 2}px) - ${tickSize / 2}px)`,
|
|
242
|
+
top: "50%",
|
|
243
|
+
transform: "translateY(-50%)",
|
|
244
|
+
}
|
|
245
|
+
: {
|
|
246
|
+
// Vertical: bottom=0%, top=100% (inverted Y-axis)
|
|
247
|
+
bottom: `calc(${trackInset}px + ${tickPercent} * (100% - ${trackInset * 2}px) - ${tickSize / 2}px)`,
|
|
248
|
+
left: "50%",
|
|
249
|
+
transform: "translateX(-50%)",
|
|
250
|
+
}),
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
return <div key={tick} style={style} aria-hidden="true" />;
|
|
254
|
+
})}
|
|
255
|
+
</>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ─── SliderTrack ──────────────────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* MD3 Expressive Slider Track.
|
|
263
|
+
*/
|
|
264
|
+
export const SliderTrack = React.memo(function SliderTrack({
|
|
265
|
+
percent,
|
|
266
|
+
trackSize,
|
|
267
|
+
orientation,
|
|
268
|
+
variant,
|
|
269
|
+
isCentered,
|
|
270
|
+
min,
|
|
271
|
+
max,
|
|
272
|
+
disabled,
|
|
273
|
+
trackRef,
|
|
274
|
+
onTrackPointerDown,
|
|
275
|
+
ticks = [],
|
|
276
|
+
insetIcon,
|
|
277
|
+
insetIconAtMin,
|
|
278
|
+
insetIconTrailing,
|
|
279
|
+
insetIconAtMax,
|
|
280
|
+
value,
|
|
281
|
+
trackShape = "md3",
|
|
282
|
+
}: Omit<SliderTrackProps, "step"> & { ticks?: number[] }) {
|
|
283
|
+
const isHorizontal = orientation === "horizontal";
|
|
284
|
+
const size = SliderTokens.trackSizes[trackSize];
|
|
285
|
+
const thumbHeight = SliderTokens.thumbHeights[trackSize];
|
|
286
|
+
const { thumbGap, thumbWidthDefault, trackInnerRadius } = SliderTokens;
|
|
287
|
+
const innerR = trackInnerRadius;
|
|
288
|
+
|
|
289
|
+
let outerR = size / 2;
|
|
290
|
+
if (trackShape === "md3") {
|
|
291
|
+
outerR = Math.min(SliderTokens.trackShapes[trackSize], size / 2);
|
|
292
|
+
} else if (typeof trackShape === "number") {
|
|
293
|
+
outerR = Math.min(trackShape, size / 2);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const thumbHalfWidth = thumbWidthDefault / 2;
|
|
297
|
+
const gapWithThumbStr = `${thumbGap + thumbHalfWidth}px`;
|
|
298
|
+
|
|
299
|
+
// ── Inset icon state ─────────────────────────────────────────────────────
|
|
300
|
+
const hasAnyInsetIcon = Boolean(insetIcon || insetIconTrailing);
|
|
301
|
+
const prefersReduced = useReducedMotion() ?? false;
|
|
302
|
+
|
|
303
|
+
// Measure actual track width to compute placement threshold.
|
|
304
|
+
const [trackWidth, setTrackWidth] = React.useState(0);
|
|
305
|
+
React.useLayoutEffect(() => {
|
|
306
|
+
const el = trackRef.current;
|
|
307
|
+
if (!el || !hasAnyInsetIcon) return;
|
|
308
|
+
// Initial measure
|
|
309
|
+
setTrackWidth(isHorizontal ? el.clientWidth : el.clientHeight);
|
|
310
|
+
// Update on resize
|
|
311
|
+
const ro = new ResizeObserver(() => {
|
|
312
|
+
setTrackWidth(isHorizontal ? el.clientWidth : el.clientHeight);
|
|
313
|
+
});
|
|
314
|
+
ro.observe(el);
|
|
315
|
+
return () => ro.disconnect();
|
|
316
|
+
}, [hasAnyInsetIcon, isHorizontal, trackRef]);
|
|
317
|
+
|
|
318
|
+
// Minimum percent required to keep icon on the active segment.
|
|
319
|
+
// = (iconSize + 2*padding + gap + halfThumb) / trackWidth
|
|
320
|
+
const activeIconSize = Math.min(
|
|
321
|
+
SliderTokens.insetIconSizes[trackSize],
|
|
322
|
+
Math.max(4, size - SliderTokens.insetIconPadding * 2),
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
const iconTotalWidth =
|
|
326
|
+
activeIconSize +
|
|
327
|
+
SliderTokens.insetIconPadding * 2 +
|
|
328
|
+
thumbGap +
|
|
329
|
+
thumbHalfWidth;
|
|
330
|
+
const iconThreshold = trackWidth > 0 ? iconTotalWidth / trackWidth : 0.15;
|
|
331
|
+
|
|
332
|
+
// Ref-based hysteresis — avoids a setState re-render cycle (which caused a
|
|
333
|
+
// 1-frame lag flicker). A dead-zone of 4% prevents rapid toggling when the
|
|
334
|
+
// thumb is near the switch boundary during fast drags.
|
|
335
|
+
// Enter active when: remaining space < threshold
|
|
336
|
+
// Exit active when: remaining space > threshold + 4% (dead-zone)
|
|
337
|
+
const HYSTERESIS_GAP = 0.04;
|
|
338
|
+
const trailingActiveRef = React.useRef(1 - percent <= iconThreshold);
|
|
339
|
+
const leadingActiveRef = React.useRef(percent > iconThreshold);
|
|
340
|
+
|
|
341
|
+
// Trailing icon hysteresis
|
|
342
|
+
const trailingPercent = 1 - percent;
|
|
343
|
+
if (trailingActiveRef.current) {
|
|
344
|
+
if (trailingPercent > iconThreshold + HYSTERESIS_GAP) {
|
|
345
|
+
trailingActiveRef.current = false;
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
if (trailingPercent <= iconThreshold) {
|
|
349
|
+
trailingActiveRef.current = true;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const trailingOnActive = trailingActiveRef.current;
|
|
353
|
+
|
|
354
|
+
// Leading icon hysteresis
|
|
355
|
+
if (leadingActiveRef.current) {
|
|
356
|
+
if (percent <= iconThreshold - HYSTERESIS_GAP) {
|
|
357
|
+
leadingActiveRef.current = false;
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
if (percent > iconThreshold) {
|
|
361
|
+
leadingActiveRef.current = true;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const leadingOnActive = leadingActiveRef.current;
|
|
365
|
+
|
|
366
|
+
// Resolve which icon to show (swap at min)
|
|
367
|
+
const isAtMin = value !== undefined && value <= min;
|
|
368
|
+
const resolvedLeadingIcon =
|
|
369
|
+
isAtMin && insetIconAtMin ? insetIconAtMin : insetIcon;
|
|
370
|
+
|
|
371
|
+
const isAtMax = value !== undefined && value >= max;
|
|
372
|
+
const resolvedTrailingIcon =
|
|
373
|
+
isAtMax && insetIconAtMax ? insetIconAtMax : insetIconTrailing;
|
|
374
|
+
|
|
375
|
+
// ── Colors ───────────────────────────────────────────────────────────────
|
|
376
|
+
const activeColor = disabled
|
|
377
|
+
? SliderColors.disabledActiveTrack
|
|
378
|
+
: `var(--md-sys-color-${variant})`;
|
|
379
|
+
const inactiveColor = disabled
|
|
380
|
+
? SliderColors.disabledInactiveTrack
|
|
381
|
+
: `var(--md-sys-color-${variant}-container)`;
|
|
382
|
+
const insetLimit = SliderTokens.thumbGap + SliderTokens.thumbWidthDefault / 2;
|
|
383
|
+
const trackInset = Math.min(size / 2, insetLimit);
|
|
384
|
+
|
|
385
|
+
// Icon positions in CSS (computed after trackInset is available)
|
|
386
|
+
const gapTotal = thumbGap + thumbHalfWidth;
|
|
387
|
+
const thumbCenter = `calc(${trackInset}px + ${percent} * (100% - ${trackInset * 2}px))`;
|
|
388
|
+
|
|
389
|
+
// Leading Icon positions
|
|
390
|
+
const leadingActiveLeft = `${trackInset + SliderTokens.insetIconPadding}px`;
|
|
391
|
+
const leadingInactiveLeft = `calc(${thumbCenter} + ${gapTotal}px + ${SliderTokens.insetIconPadding}px)`;
|
|
392
|
+
|
|
393
|
+
// Trailing Icon positions
|
|
394
|
+
const trailingInactiveLeft = `calc(100% - ${trackInset}px - ${activeIconSize}px - ${SliderTokens.insetIconPadding}px)`;
|
|
395
|
+
const trailingActiveLeft = `calc(${thumbCenter} - ${gapTotal}px - ${activeIconSize}px - ${SliderTokens.insetIconPadding}px)`;
|
|
396
|
+
|
|
397
|
+
// ── Horizontal layout ────────────────────────────────────────────────────
|
|
398
|
+
if (isHorizontal) {
|
|
399
|
+
const cxStr = `calc(${trackInset}px + ${percent} * (100% - ${trackInset * 2}px))`;
|
|
400
|
+
|
|
401
|
+
const segments: React.ReactNode[] = [];
|
|
402
|
+
|
|
403
|
+
if (!isCentered) {
|
|
404
|
+
const leftSegmentWidth = `max(0px, calc(${cxStr} - ${gapWithThumbStr}))`;
|
|
405
|
+
const rightSegmentLeft = `calc(${cxStr} + ${gapWithThumbStr})`;
|
|
406
|
+
const rightSegmentWidth = `max(0px, calc(100% - (${cxStr} + ${gapWithThumbStr})))`;
|
|
407
|
+
|
|
408
|
+
// Leading Segment
|
|
409
|
+
segments.push(
|
|
410
|
+
<div
|
|
411
|
+
key="left"
|
|
412
|
+
aria-hidden="true"
|
|
413
|
+
style={{
|
|
414
|
+
position: "absolute",
|
|
415
|
+
left: 0,
|
|
416
|
+
top: "50%",
|
|
417
|
+
transform: "translateY(-50%)",
|
|
418
|
+
width: leftSegmentWidth,
|
|
419
|
+
height: size,
|
|
420
|
+
backgroundColor: activeColor,
|
|
421
|
+
opacity: disabled ? 0.38 : 1,
|
|
422
|
+
...getHorizontalRadius(true, innerR, outerR),
|
|
423
|
+
}}
|
|
424
|
+
/>,
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
// Trailing Segment
|
|
428
|
+
segments.push(
|
|
429
|
+
<div
|
|
430
|
+
key="right"
|
|
431
|
+
aria-hidden="true"
|
|
432
|
+
style={{
|
|
433
|
+
position: "absolute",
|
|
434
|
+
left: rightSegmentLeft,
|
|
435
|
+
top: "50%",
|
|
436
|
+
transform: "translateY(-50%)",
|
|
437
|
+
width: rightSegmentWidth,
|
|
438
|
+
height: size,
|
|
439
|
+
backgroundColor: inactiveColor,
|
|
440
|
+
opacity: disabled ? 0.38 : 1,
|
|
441
|
+
...getHorizontalRadius(false, innerR, outerR),
|
|
442
|
+
}}
|
|
443
|
+
/>,
|
|
444
|
+
);
|
|
445
|
+
} else {
|
|
446
|
+
// Centered mode
|
|
447
|
+
const halfCenterGap = SliderTokens.thumbGap / 2;
|
|
448
|
+
|
|
449
|
+
if (percent >= 0.5) {
|
|
450
|
+
// Left base segment (Inactive)
|
|
451
|
+
const leftBaseWidth = `max(0px, min(calc(50% - ${halfCenterGap}px), calc(${cxStr} - ${gapWithThumbStr})))`;
|
|
452
|
+
segments.push(
|
|
453
|
+
<div
|
|
454
|
+
key="left-base"
|
|
455
|
+
aria-hidden="true"
|
|
456
|
+
style={{
|
|
457
|
+
position: "absolute",
|
|
458
|
+
left: 0,
|
|
459
|
+
top: "50%",
|
|
460
|
+
transform: "translateY(-50%)",
|
|
461
|
+
width: leftBaseWidth,
|
|
462
|
+
height: size,
|
|
463
|
+
backgroundColor: inactiveColor,
|
|
464
|
+
opacity: disabled ? 0.38 : 1,
|
|
465
|
+
...getHorizontalRadius(true, innerR, outerR),
|
|
466
|
+
}}
|
|
467
|
+
/>,
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
// Center active segment
|
|
471
|
+
const centerActiveLeft = `calc(50% + ${halfCenterGap}px)`;
|
|
472
|
+
const centerActiveWidth = `max(0px, calc(${cxStr} - ${gapWithThumbStr} - (50% + ${halfCenterGap}px)))`;
|
|
473
|
+
segments.push(
|
|
474
|
+
<div
|
|
475
|
+
key="center-active"
|
|
476
|
+
aria-hidden="true"
|
|
477
|
+
style={{
|
|
478
|
+
position: "absolute",
|
|
479
|
+
left: centerActiveLeft,
|
|
480
|
+
top: "50%",
|
|
481
|
+
transform: "translateY(-50%)",
|
|
482
|
+
width: centerActiveWidth,
|
|
483
|
+
height: size,
|
|
484
|
+
backgroundColor: activeColor,
|
|
485
|
+
opacity: disabled ? 0.38 : 1,
|
|
486
|
+
...allInnerRadius(innerR),
|
|
487
|
+
}}
|
|
488
|
+
/>,
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
// Right base segment (Inactive)
|
|
492
|
+
const rightBaseLeft = `calc(${cxStr} + ${gapWithThumbStr})`;
|
|
493
|
+
const rightBaseWidth = `max(0px, calc(100% - (${cxStr} + ${gapWithThumbStr})))`;
|
|
494
|
+
segments.push(
|
|
495
|
+
<div
|
|
496
|
+
key="right-base"
|
|
497
|
+
aria-hidden="true"
|
|
498
|
+
style={{
|
|
499
|
+
position: "absolute",
|
|
500
|
+
left: rightBaseLeft,
|
|
501
|
+
top: "50%",
|
|
502
|
+
transform: "translateY(-50%)",
|
|
503
|
+
width: rightBaseWidth,
|
|
504
|
+
height: size,
|
|
505
|
+
backgroundColor: inactiveColor,
|
|
506
|
+
opacity: disabled ? 0.38 : 1,
|
|
507
|
+
...getHorizontalRadius(false, innerR, outerR),
|
|
508
|
+
}}
|
|
509
|
+
/>,
|
|
510
|
+
);
|
|
511
|
+
} else {
|
|
512
|
+
// Left base segment (Inactive)
|
|
513
|
+
const leftBaseWidth = `max(0px, calc(${cxStr} - ${gapWithThumbStr}))`;
|
|
514
|
+
segments.push(
|
|
515
|
+
<div
|
|
516
|
+
key="left-base"
|
|
517
|
+
aria-hidden="true"
|
|
518
|
+
style={{
|
|
519
|
+
position: "absolute",
|
|
520
|
+
left: 0,
|
|
521
|
+
top: "50%",
|
|
522
|
+
transform: "translateY(-50%)",
|
|
523
|
+
width: leftBaseWidth,
|
|
524
|
+
height: size,
|
|
525
|
+
backgroundColor: inactiveColor,
|
|
526
|
+
opacity: disabled ? 0.38 : 1,
|
|
527
|
+
...getHorizontalRadius(true, innerR, outerR),
|
|
528
|
+
}}
|
|
529
|
+
/>,
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
// Center active segment
|
|
533
|
+
const centerActiveLeft = `calc(${cxStr} + ${gapWithThumbStr})`;
|
|
534
|
+
const centerActiveWidth = `max(0px, calc(50% - ${halfCenterGap}px - (${cxStr} + ${gapWithThumbStr})))`;
|
|
535
|
+
segments.push(
|
|
536
|
+
<div
|
|
537
|
+
key="center-active"
|
|
538
|
+
aria-hidden="true"
|
|
539
|
+
style={{
|
|
540
|
+
position: "absolute",
|
|
541
|
+
left: centerActiveLeft,
|
|
542
|
+
top: "50%",
|
|
543
|
+
transform: "translateY(-50%)",
|
|
544
|
+
width: centerActiveWidth,
|
|
545
|
+
height: size,
|
|
546
|
+
backgroundColor: activeColor,
|
|
547
|
+
opacity: disabled ? 0.38 : 1,
|
|
548
|
+
...allInnerRadius(innerR),
|
|
549
|
+
}}
|
|
550
|
+
/>,
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
// Right base segment (Inactive)
|
|
554
|
+
const rightBaseLeft = `max(calc(50% + ${halfCenterGap}px), calc(${cxStr} + ${gapWithThumbStr}))`;
|
|
555
|
+
const rightBaseWidth = `max(0px, calc(100% - max(calc(50% + ${halfCenterGap}px), calc(${cxStr} + ${gapWithThumbStr}))))`;
|
|
556
|
+
segments.push(
|
|
557
|
+
<div
|
|
558
|
+
key="right-base"
|
|
559
|
+
aria-hidden="true"
|
|
560
|
+
style={{
|
|
561
|
+
position: "absolute",
|
|
562
|
+
left: rightBaseLeft,
|
|
563
|
+
top: "50%",
|
|
564
|
+
transform: "translateY(-50%)",
|
|
565
|
+
width: rightBaseWidth,
|
|
566
|
+
height: size,
|
|
567
|
+
backgroundColor: inactiveColor,
|
|
568
|
+
opacity: disabled ? 0.38 : 1,
|
|
569
|
+
...getHorizontalRadius(false, innerR, outerR),
|
|
570
|
+
}}
|
|
571
|
+
/>,
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return (
|
|
577
|
+
<div
|
|
578
|
+
ref={trackRef}
|
|
579
|
+
className={cn(
|
|
580
|
+
"relative w-full",
|
|
581
|
+
disabled ? "cursor-not-allowed" : "cursor-pointer",
|
|
582
|
+
)}
|
|
583
|
+
style={{ height: thumbHeight }}
|
|
584
|
+
onPointerDown={onTrackPointerDown}
|
|
585
|
+
aria-hidden="true"
|
|
586
|
+
>
|
|
587
|
+
{segments}
|
|
588
|
+
{ticks.length > 0 && (
|
|
589
|
+
<Ticks
|
|
590
|
+
ticks={ticks}
|
|
591
|
+
min={min}
|
|
592
|
+
max={max}
|
|
593
|
+
percent={percent}
|
|
594
|
+
orientation={orientation}
|
|
595
|
+
disabled={disabled}
|
|
596
|
+
variant={variant}
|
|
597
|
+
isCentered={isCentered}
|
|
598
|
+
trackInset={trackInset}
|
|
599
|
+
/>
|
|
600
|
+
)}
|
|
601
|
+
{/* Inset Icons (Leading & Trailing) */}
|
|
602
|
+
{/* Leading icon: key changes on min-swap OR active↔inactive swap → fade transition */}
|
|
603
|
+
<AnimatePresence mode="wait">
|
|
604
|
+
{resolvedLeadingIcon && (
|
|
605
|
+
<InsetIcon
|
|
606
|
+
key={
|
|
607
|
+
isAtMin
|
|
608
|
+
? "lead-min"
|
|
609
|
+
: leadingOnActive
|
|
610
|
+
? "lead-active"
|
|
611
|
+
: "lead-inactive"
|
|
612
|
+
}
|
|
613
|
+
icon={resolvedLeadingIcon}
|
|
614
|
+
isOnActiveSegment={leadingOnActive}
|
|
615
|
+
position={
|
|
616
|
+
leadingOnActive ? leadingActiveLeft : leadingInactiveLeft
|
|
617
|
+
}
|
|
618
|
+
orientation={orientation}
|
|
619
|
+
trackSize={size}
|
|
620
|
+
trackSizeToken={trackSize}
|
|
621
|
+
disabled={disabled}
|
|
622
|
+
variant={variant}
|
|
623
|
+
prefersReduced={prefersReduced}
|
|
624
|
+
/>
|
|
625
|
+
)}
|
|
626
|
+
</AnimatePresence>
|
|
627
|
+
|
|
628
|
+
{/* Trailing icon: key changes on max-swap OR active↔inactive swap → fade transition */}
|
|
629
|
+
<AnimatePresence mode="wait">
|
|
630
|
+
{resolvedTrailingIcon && (
|
|
631
|
+
<InsetIcon
|
|
632
|
+
key={
|
|
633
|
+
isAtMax
|
|
634
|
+
? "trail-max"
|
|
635
|
+
: trailingOnActive
|
|
636
|
+
? "trail-active"
|
|
637
|
+
: "trail-inactive"
|
|
638
|
+
}
|
|
639
|
+
icon={resolvedTrailingIcon}
|
|
640
|
+
isOnActiveSegment={trailingOnActive}
|
|
641
|
+
position={
|
|
642
|
+
trailingOnActive ? trailingActiveLeft : trailingInactiveLeft
|
|
643
|
+
}
|
|
644
|
+
orientation={orientation}
|
|
645
|
+
trackSize={size}
|
|
646
|
+
trackSizeToken={trackSize}
|
|
647
|
+
disabled={disabled}
|
|
648
|
+
variant={variant}
|
|
649
|
+
prefersReduced={prefersReduced}
|
|
650
|
+
/>
|
|
651
|
+
)}
|
|
652
|
+
</AnimatePresence>
|
|
653
|
+
</div>
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// ── Vertical layout ───────────────────────────────────────────────────────
|
|
658
|
+
const cyStr = `calc(${trackInset}px + ${percent} * (100% - ${trackInset * 2}px))`;
|
|
659
|
+
|
|
660
|
+
const segments: React.ReactNode[] = [];
|
|
661
|
+
|
|
662
|
+
if (!isCentered) {
|
|
663
|
+
const bottomSegmentHeight = `max(0px, calc(${cyStr} - ${gapWithThumbStr}))`;
|
|
664
|
+
const topSegmentBottom = `calc(${cyStr} + ${gapWithThumbStr})`;
|
|
665
|
+
const topSegmentHeight = `max(0px, calc(100% - (${cyStr} + ${gapWithThumbStr})))`;
|
|
666
|
+
|
|
667
|
+
// Bottom segment
|
|
668
|
+
segments.push(
|
|
669
|
+
<div
|
|
670
|
+
key="bottom"
|
|
671
|
+
aria-hidden="true"
|
|
672
|
+
style={{
|
|
673
|
+
position: "absolute",
|
|
674
|
+
bottom: 0,
|
|
675
|
+
left: "50%",
|
|
676
|
+
transform: "translateX(-50%)",
|
|
677
|
+
height: bottomSegmentHeight,
|
|
678
|
+
width: size,
|
|
679
|
+
backgroundColor: activeColor,
|
|
680
|
+
opacity: disabled ? 0.38 : 1,
|
|
681
|
+
...getVerticalRadius(true, innerR, outerR),
|
|
682
|
+
}}
|
|
683
|
+
/>,
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
// Top segment
|
|
687
|
+
segments.push(
|
|
688
|
+
<div
|
|
689
|
+
key="top"
|
|
690
|
+
aria-hidden="true"
|
|
691
|
+
style={{
|
|
692
|
+
position: "absolute",
|
|
693
|
+
bottom: topSegmentBottom,
|
|
694
|
+
left: "50%",
|
|
695
|
+
transform: "translateX(-50%)",
|
|
696
|
+
height: topSegmentHeight,
|
|
697
|
+
width: size,
|
|
698
|
+
backgroundColor: inactiveColor,
|
|
699
|
+
opacity: disabled ? 0.38 : 1,
|
|
700
|
+
...getVerticalRadius(false, innerR, outerR),
|
|
701
|
+
}}
|
|
702
|
+
/>,
|
|
703
|
+
);
|
|
704
|
+
} else {
|
|
705
|
+
// Centered mode (Vertical is inverted: bottom=0%, top=100%)
|
|
706
|
+
const halfCenterGap = SliderTokens.thumbGap / 2;
|
|
707
|
+
|
|
708
|
+
if (percent >= 0.5) {
|
|
709
|
+
// Bottom base segment (Inactive)
|
|
710
|
+
const bottomBaseHeight = `max(0px, min(calc(50% - ${halfCenterGap}px), calc(${cyStr} - ${gapWithThumbStr})))`;
|
|
711
|
+
segments.push(
|
|
712
|
+
<div
|
|
713
|
+
key="bottom-base"
|
|
714
|
+
aria-hidden="true"
|
|
715
|
+
style={{
|
|
716
|
+
position: "absolute",
|
|
717
|
+
bottom: 0,
|
|
718
|
+
left: "50%",
|
|
719
|
+
transform: "translateX(-50%)",
|
|
720
|
+
height: bottomBaseHeight,
|
|
721
|
+
width: size,
|
|
722
|
+
backgroundColor: inactiveColor,
|
|
723
|
+
opacity: disabled ? 0.38 : 1,
|
|
724
|
+
...getVerticalRadius(true, innerR, outerR),
|
|
725
|
+
}}
|
|
726
|
+
/>,
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
// Center active segment
|
|
730
|
+
const centerActiveBottom = `calc(50% + ${halfCenterGap}px)`;
|
|
731
|
+
const centerActiveHeight = `max(0px, calc(${cyStr} - ${gapWithThumbStr} - (50% + ${halfCenterGap}px)))`;
|
|
732
|
+
segments.push(
|
|
733
|
+
<div
|
|
734
|
+
key="center-active"
|
|
735
|
+
aria-hidden="true"
|
|
736
|
+
style={{
|
|
737
|
+
position: "absolute",
|
|
738
|
+
bottom: centerActiveBottom,
|
|
739
|
+
left: "50%",
|
|
740
|
+
transform: "translateX(-50%)",
|
|
741
|
+
height: centerActiveHeight,
|
|
742
|
+
width: size,
|
|
743
|
+
backgroundColor: activeColor,
|
|
744
|
+
opacity: disabled ? 0.38 : 1,
|
|
745
|
+
...allInnerRadius(innerR),
|
|
746
|
+
}}
|
|
747
|
+
/>,
|
|
748
|
+
);
|
|
749
|
+
|
|
750
|
+
// Top base segment (Inactive)
|
|
751
|
+
const topBaseBottom = `calc(${cyStr} + ${gapWithThumbStr})`;
|
|
752
|
+
const topBaseHeight = `max(0px, calc(100% - (${cyStr} + ${gapWithThumbStr})))`;
|
|
753
|
+
segments.push(
|
|
754
|
+
<div
|
|
755
|
+
key="top-base"
|
|
756
|
+
aria-hidden="true"
|
|
757
|
+
style={{
|
|
758
|
+
position: "absolute",
|
|
759
|
+
bottom: topBaseBottom,
|
|
760
|
+
left: "50%",
|
|
761
|
+
transform: "translateX(-50%)",
|
|
762
|
+
height: topBaseHeight,
|
|
763
|
+
width: size,
|
|
764
|
+
backgroundColor: inactiveColor,
|
|
765
|
+
opacity: disabled ? 0.38 : 1,
|
|
766
|
+
...getVerticalRadius(false, innerR, outerR),
|
|
767
|
+
}}
|
|
768
|
+
/>,
|
|
769
|
+
);
|
|
770
|
+
} else {
|
|
771
|
+
// Bottom base segment (Inactive)
|
|
772
|
+
const bottomBaseHeight = `max(0px, calc(${cyStr} - ${gapWithThumbStr}))`;
|
|
773
|
+
segments.push(
|
|
774
|
+
<div
|
|
775
|
+
key="bottom-base"
|
|
776
|
+
aria-hidden="true"
|
|
777
|
+
style={{
|
|
778
|
+
position: "absolute",
|
|
779
|
+
bottom: 0,
|
|
780
|
+
left: "50%",
|
|
781
|
+
transform: "translateX(-50%)",
|
|
782
|
+
height: bottomBaseHeight,
|
|
783
|
+
width: size,
|
|
784
|
+
backgroundColor: inactiveColor,
|
|
785
|
+
opacity: disabled ? 0.38 : 1,
|
|
786
|
+
...getVerticalRadius(true, innerR, outerR),
|
|
787
|
+
}}
|
|
788
|
+
/>,
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
// Center active segment
|
|
792
|
+
const centerActiveBottom = `calc(${cyStr} + ${gapWithThumbStr})`;
|
|
793
|
+
const centerActiveHeight = `max(0px, calc(50% - ${halfCenterGap}px - (${cyStr} + ${gapWithThumbStr})))`;
|
|
794
|
+
segments.push(
|
|
795
|
+
<div
|
|
796
|
+
key="center-active"
|
|
797
|
+
aria-hidden="true"
|
|
798
|
+
style={{
|
|
799
|
+
position: "absolute",
|
|
800
|
+
bottom: centerActiveBottom,
|
|
801
|
+
left: "50%",
|
|
802
|
+
transform: "translateX(-50%)",
|
|
803
|
+
height: centerActiveHeight,
|
|
804
|
+
width: size,
|
|
805
|
+
backgroundColor: activeColor,
|
|
806
|
+
opacity: disabled ? 0.38 : 1,
|
|
807
|
+
...allInnerRadius(innerR),
|
|
808
|
+
}}
|
|
809
|
+
/>,
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
// Top base segment (Inactive)
|
|
813
|
+
const topBaseBottom = `max(calc(50% + ${halfCenterGap}px), calc(${cyStr} + ${gapWithThumbStr}))`;
|
|
814
|
+
const topBaseHeight = `max(0px, calc(100% - max(calc(50% + ${halfCenterGap}px), calc(${cyStr} + ${gapWithThumbStr}))))`;
|
|
815
|
+
segments.push(
|
|
816
|
+
<div
|
|
817
|
+
key="top-base"
|
|
818
|
+
aria-hidden="true"
|
|
819
|
+
style={{
|
|
820
|
+
position: "absolute",
|
|
821
|
+
bottom: topBaseBottom,
|
|
822
|
+
left: "50%",
|
|
823
|
+
transform: "translateX(-50%)",
|
|
824
|
+
height: topBaseHeight,
|
|
825
|
+
width: size,
|
|
826
|
+
backgroundColor: inactiveColor,
|
|
827
|
+
opacity: disabled ? 0.38 : 1,
|
|
828
|
+
...getVerticalRadius(false, innerR, outerR),
|
|
829
|
+
}}
|
|
830
|
+
/>,
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return (
|
|
836
|
+
<div
|
|
837
|
+
ref={trackRef}
|
|
838
|
+
className={cn(
|
|
839
|
+
"relative h-full",
|
|
840
|
+
disabled ? "cursor-not-allowed" : "cursor-pointer",
|
|
841
|
+
)}
|
|
842
|
+
style={{ width: thumbHeight }}
|
|
843
|
+
onPointerDown={onTrackPointerDown}
|
|
844
|
+
aria-hidden="true"
|
|
845
|
+
>
|
|
846
|
+
{segments}
|
|
847
|
+
{ticks.length > 0 && (
|
|
848
|
+
<Ticks
|
|
849
|
+
ticks={ticks}
|
|
850
|
+
min={min}
|
|
851
|
+
max={max}
|
|
852
|
+
percent={percent}
|
|
853
|
+
orientation={orientation}
|
|
854
|
+
disabled={disabled}
|
|
855
|
+
variant={variant}
|
|
856
|
+
isCentered={isCentered}
|
|
857
|
+
trackInset={trackInset}
|
|
858
|
+
/>
|
|
859
|
+
)}
|
|
860
|
+
{/* Inset Icons (Leading & Trailing) */}
|
|
861
|
+
{/* Leading icon: key changes on min-swap OR active↔inactive swap → fade transition */}
|
|
862
|
+
<AnimatePresence mode="wait">
|
|
863
|
+
{resolvedLeadingIcon && (
|
|
864
|
+
<InsetIcon
|
|
865
|
+
key={
|
|
866
|
+
isAtMin
|
|
867
|
+
? "lead-min"
|
|
868
|
+
: leadingOnActive
|
|
869
|
+
? "lead-active"
|
|
870
|
+
: "lead-inactive"
|
|
871
|
+
}
|
|
872
|
+
icon={resolvedLeadingIcon}
|
|
873
|
+
isOnActiveSegment={leadingOnActive}
|
|
874
|
+
position={leadingOnActive ? leadingActiveLeft : leadingInactiveLeft}
|
|
875
|
+
orientation={orientation}
|
|
876
|
+
trackSize={size}
|
|
877
|
+
trackSizeToken={trackSize}
|
|
878
|
+
disabled={disabled}
|
|
879
|
+
variant={variant}
|
|
880
|
+
prefersReduced={prefersReduced}
|
|
881
|
+
/>
|
|
882
|
+
)}
|
|
883
|
+
</AnimatePresence>
|
|
884
|
+
|
|
885
|
+
{/* Trailing icon: key changes on max-swap OR active↔inactive swap → fade transition */}
|
|
886
|
+
<AnimatePresence mode="wait">
|
|
887
|
+
{resolvedTrailingIcon && (
|
|
888
|
+
<InsetIcon
|
|
889
|
+
key={
|
|
890
|
+
isAtMax
|
|
891
|
+
? "trail-max"
|
|
892
|
+
: trailingOnActive
|
|
893
|
+
? "trail-active"
|
|
894
|
+
: "trail-inactive"
|
|
895
|
+
}
|
|
896
|
+
icon={resolvedTrailingIcon}
|
|
897
|
+
isOnActiveSegment={trailingOnActive}
|
|
898
|
+
position={
|
|
899
|
+
trailingOnActive ? trailingActiveLeft : trailingInactiveLeft
|
|
900
|
+
}
|
|
901
|
+
orientation={orientation}
|
|
902
|
+
trackSize={size}
|
|
903
|
+
trackSizeToken={trackSize}
|
|
904
|
+
disabled={disabled}
|
|
905
|
+
variant={variant}
|
|
906
|
+
prefersReduced={prefersReduced}
|
|
907
|
+
/>
|
|
908
|
+
)}
|
|
909
|
+
</AnimatePresence>
|
|
910
|
+
</div>
|
|
911
|
+
);
|
|
912
|
+
});
|