@bug-on/md3-react 1.0.0 → 2.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.
@@ -53,28 +53,9 @@
53
53
  }
54
54
 
55
55
  /*
56
- * Rounded variant uncomment nếu dùng
57
- *
58
- @font-face {
59
- font-family: 'Material Symbols Rounded';
60
- font-style: normal;
61
- font-weight: 100 700;
62
- font-display: block;
63
- src: url('/fonts/MaterialSymbolsRounded[FILL,GRAD,opsz,wght].woff2') format('woff2');
64
- }
65
- */
66
-
67
- /*
68
- * Sharp variant — uncomment nếu dùng
69
- *
70
- @font-face {
71
- font-family: 'Material Symbols Sharp';
72
- font-style: normal;
73
- font-weight: 100 700;
74
- font-display: block;
75
- src: url('/fonts/MaterialSymbolsSharp[FILL,GRAD,opsz,wght].woff2') format('woff2');
76
- }
77
- */
56
+ * Rounded variants and Sharp variants are not included by default to save space.
57
+ * You can declare them similarly if you download the respective woff2 files.
58
+ */
78
59
 
79
60
  /* ─────────────────────────────────────────────────────────────────────────────
80
61
  * BASE ICON STYLES
@@ -0,0 +1,101 @@
1
+ /**
2
+ * @file useSliderMath.ts
3
+ * MD3 Expressive Slider — Math utility hook.
4
+ *
5
+ * Handles all slider math in one place:
6
+ * - value coercion to [min, max]
7
+ * - step snapping for discrete mode
8
+ * - value ↔ percent conversion
9
+ * - keyboard delta calculation
10
+ * - tick position generation
11
+ *
12
+ * Exported as a pure hook for testability and reuse in both Slider and RangeSlider.
13
+ */
14
+ /**
15
+ * Clamps `value` to the closed interval `[min, max]`.
16
+ *
17
+ * @example coerceValue(150, 0, 100) → 100
18
+ */
19
+ export declare function coerceValue(value: number, min: number, max: number): number;
20
+ /**
21
+ * Rounds `value` to the nearest multiple of `step` relative to `min`.
22
+ * When `step === 0`, returns `value` unchanged (continuous mode).
23
+ *
24
+ * @example snapToStep(23, 0, 10) → 20
25
+ * @example snapToStep(27, 0, 10) → 30
26
+ */
27
+ export declare function snapToStep(value: number, min: number, step: number): number;
28
+ /**
29
+ * Converts a raw value to a 0–1 fraction of the [min, max] range.
30
+ * Returns 0 when max === min (degenerate range).
31
+ *
32
+ * @example valueToPercent(50, 0, 100) → 0.5
33
+ */
34
+ export declare function valueToPercent(value: number, min: number, max: number): number;
35
+ /**
36
+ * Converts a 0–1 fraction to a value within [min, max], then snaps to step.
37
+ * The result is also coerced to [min, max].
38
+ *
39
+ * @example percentToValue(0.5, 0, 100, 10) → 50
40
+ * @example percentToValue(0.23, 0, 100, 10) → 20
41
+ */
42
+ export declare function percentToValue(percent: number, min: number, max: number, step: number): number;
43
+ /**
44
+ * Computes the keyboard increment for a given key.
45
+ *
46
+ * Key mappings per WAI-ARIA Slider pattern:
47
+ * - ArrowRight/Up → +step (or +1% of range if continuous)
48
+ * - ArrowLeft/Down → -step (or -1% of range)
49
+ * - PageUp → +10% of range (snapped to step)
50
+ * - PageDown → -10% of range (snapped to step)
51
+ * - Home / End → handled by the caller (jump to min/max)
52
+ *
53
+ * @returns the signed delta to add to current value, or `null` for Home/End.
54
+ */
55
+ export declare function getKeyboardDelta(key: string, step: number, min: number, max: number): number | null;
56
+ /**
57
+ * Generates the list of value positions for tick marks.
58
+ * Returns an empty array when `step === 0` (continuous mode).
59
+ *
60
+ * @example generateTicks(0, 100, 10) → [0, 10, 20, ..., 100]
61
+ */
62
+ export declare function generateTicks(min: number, max: number, step: number): number[];
63
+ export interface UseSliderMathOptions {
64
+ min: number;
65
+ max: number;
66
+ step: number;
67
+ }
68
+ export interface UseSliderMathReturn {
69
+ /** Clamp value to [min, max]. */
70
+ coerce: (v: number) => number;
71
+ /** Snap value to nearest step (no-op if step=0). */
72
+ snap: (v: number) => number;
73
+ /** Value → percent [0, 1]. */
74
+ toPercent: (v: number) => number;
75
+ /** Percent [0, 1] → snapped value. */
76
+ fromPercent: (pct: number) => number;
77
+ /**
78
+ * Signed delta for a keyboard key.
79
+ * Returns `null` for Home/End (jump to min/max, handled by caller).
80
+ */
81
+ getKeyDelta: (key: string) => number | null;
82
+ /** Tick positions. Empty array in continuous mode. */
83
+ ticks: number[];
84
+ }
85
+ /**
86
+ * Math utility hook for MD3 Slider components.
87
+ *
88
+ * Memoizes all math functions against `min`, `max`, `step` changes.
89
+ * Use this in both `<Slider>` and `<RangeSlider>` to share logic.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * const { coerce, snap, toPercent, fromPercent, getKeyDelta, ticks } =
94
+ * useSliderMath({ min: 0, max: 100, step: 10 });
95
+ *
96
+ * const percent = toPercent(value); // → 0.5 for value=50
97
+ * const newValue = fromPercent(dragPercent); // → 50
98
+ * const delta = getKeyDelta("ArrowRight"); // → 10
99
+ * ```
100
+ */
101
+ export declare function useSliderMath({ min, max, step, }: UseSliderMathOptions): UseSliderMathReturn;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @file index.ts
3
+ * MD3 Expressive Slider — Public API exports.
4
+ */
5
+ export { RangeSlider } from "./range-slider";
6
+ export { Slider } from "./slider";
7
+ export { SliderColors, SliderTokens } from "./slider.tokens";
8
+ export type { RangeSliderProps, SliderOrientation, SliderProps, SliderTrackSize, SliderVariant, } from "./slider.types";
9
+ export { SliderThumb } from "./slider-thumb";
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @file range-slider.tsx
3
+ * MD3 Expressive RangeSlider — Two-thumb slider with crossover prevention.
4
+ *
5
+ * Extends the core Slider architecture with:
6
+ * - Two SliderThumb instances (start + end)
7
+ * - Crossover prevention: start cannot exceed end and vice versa
8
+ * - Z-index management: last-dragged thumb always on top
9
+ * - Active track spans from startPercent to endPercent
10
+ *
11
+ * Design decisions:
12
+ * - The track renders a custom "range" segment between the two thumbs.
13
+ * - Each thumb has independent onValueChange callbacks + crossover clamping.
14
+ * - Last-active thumb gets zIndex=2 to appear on top when thumbs are at same position.
15
+ *
16
+ * @see https://m3.material.io/components/sliders/overview
17
+ * @see docs/m3/sliders/Slider.kt#RangeSlider
18
+ */
19
+ import * as React from "react";
20
+ import type { RangeSliderProps } from "./slider.types";
21
+ /**
22
+ * MD3 Expressive RangeSlider component.
23
+ *
24
+ * Two-thumb slider where the active track spans between the two thumbs.
25
+ * Thumbs cannot cross each other (crossover prevention built in).
26
+ *
27
+ * @example
28
+ * ```tsx
29
+ * // Controlled
30
+ * <RangeSlider
31
+ * value={[20, 80]}
32
+ * onValueChange={([start, end]) => setRange([start, end])}
33
+ * aria-label="Price range"
34
+ * />
35
+ *
36
+ * // Discrete
37
+ * <RangeSlider defaultValue={[0, 100]} step={10} />
38
+ *
39
+ * // Vertical
40
+ * <div className="h-64">
41
+ * <RangeSlider defaultValue={[25, 75]} orientation="vertical" />
42
+ * </div>
43
+ * ```
44
+ *
45
+ * @see https://m3.material.io/components/sliders/overview
46
+ */
47
+ export declare const RangeSlider: React.NamedExoticComponent<RangeSliderProps & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @file slider-thumb.tsx
3
+ * MD3 Expressive Slider — Animated pill-shaped thumb (handle).
4
+ *
5
+ * Design decisions:
6
+ * 1. POINTER EVENTS (not Framer Motion drag): We use React pointer events
7
+ * + useMotionValue for real-time position tracking without re-render lag.
8
+ * Framer Motion's `drag` prop adds momentum/inertia that conflicts with MD3
9
+ * precise positioning; direct pointer handling gives us full control.
10
+ * 2. SQUEEZE ANIMATION: `whileTap` shrinks width from 4px → 2px via spring.
11
+ * 3. INVERTED Y-AXIS: Vertical slider maps bottom=min, top=max by inverting
12
+ * the pointer delta direction.
13
+ * 4. VALUE INDICATOR: AnimatePresence pill tooltip with teardrop origin point.
14
+ * 5. ACCESSIBILITY: role=slider, full ARIA attributes, keyboard nav.
15
+ * 6. prefers-reduced-motion: Disables all animations for accessibility.
16
+ *
17
+ * @see docs/m3/sliders/Slider.kt#SliderDefaults.Thumb
18
+ */
19
+ import * as React from "react";
20
+ import type { SliderThumbProps } from "./slider.types";
21
+ /**
22
+ * MD3 Expressive Slider Thumb (handle).
23
+ *
24
+ * - Pill shape: 4px wide × 44px tall by default.
25
+ * - Squeezes to 2px wide on press/drag (Framer Motion spring).
26
+ * - Floats above track via absolute positioning at `percent` along the track.
27
+ * - Handles pointer drag via React PointerEvents + useMotionValue.
28
+ * - Full keyboard navigation per WAI-ARIA Slider pattern.
29
+ * - Value indicator tooltip with AnimatePresence.
30
+ *
31
+ * @internal — consumed by `<Slider>` and `<RangeSlider>`
32
+ */
33
+ export declare const SliderThumb: React.NamedExoticComponent<SliderThumbProps>;
@@ -0,0 +1,25 @@
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
+ import * as React from "react";
19
+ import type { SliderTrackProps } from "./slider.types";
20
+ /**
21
+ * MD3 Expressive Slider Track.
22
+ */
23
+ export declare const SliderTrack: React.NamedExoticComponent<Omit<SliderTrackProps, "step"> & {
24
+ ticks?: number[];
25
+ }>;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @file slider.tsx
3
+ * MD3 Expressive Slider — Main composition component.
4
+ *
5
+ * Supports:
6
+ * - Controlled and uncontrolled usage patterns
7
+ * - Horizontal and vertical orientations
8
+ * - Discrete mode (step snapping + tick marks)
9
+ * - Centered active track (isCentered)
10
+ * - Value indicator tooltip
11
+ * - Full keyboard navigation (ARIA Slider pattern)
12
+ * - Click-to-jump on track
13
+ *
14
+ * Architecture: Flat (no Context needed for single thumb variant).
15
+ * Track + Thumb are internal sub-components composed here.
16
+ *
17
+ * @see https://m3.material.io/components/sliders/overview
18
+ * @see docs/m3/sliders/Slider.kt
19
+ */
20
+ import * as React from "react";
21
+ import type { SliderProps } from "./slider.types";
22
+ /**
23
+ * MD3 Expressive Slider component.
24
+ *
25
+ * A pill-shaped vertical handle that slides along a rounded track.
26
+ * Supports continuous and discrete (step) modes, horizontal/vertical
27
+ * orientations, and centered active track.
28
+ *
29
+ * Features:
30
+ * - M3 Expressive handle: 4px pill that squeezes to 2px on press
31
+ * - 6px transparent gap between track and thumb
32
+ * - Asymmetric corner radii: outer ends=9999px, inner ends=2px
33
+ * - Tick marks for discrete mode
34
+ * - Optional floating value indicator tooltip
35
+ * - Full keyboard navigation per WAI-ARIA Slider pattern
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * // Basic controlled
40
+ * <Slider value={volume} onValueChange={setVolume} aria-label="Volume" />
41
+ *
42
+ * // Discrete with ticks
43
+ * <Slider defaultValue={0} step={10} min={0} max={100} />
44
+ *
45
+ * // Vertical orientation
46
+ * <div className="h-64">
47
+ * <Slider defaultValue={50} orientation="vertical" aria-label="Brightness" />
48
+ * </div>
49
+ *
50
+ * // Centered active track
51
+ * <Slider defaultValue={0} isCentered showValueIndicator />
52
+ *
53
+ * // Large track with value tooltip
54
+ * <Slider defaultValue={50} trackSize="l" showValueIndicator
55
+ * formatValue={(v) => `${v}%`} />
56
+ * ```
57
+ *
58
+ * @see https://m3.material.io/components/sliders/overview
59
+ */
60
+ export declare const Slider: React.NamedExoticComponent<SliderProps & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,151 @@
1
+ /**
2
+ * @file slider.tokens.ts
3
+ * MD3 Expressive Slider — Design tokens ported from:
4
+ * - SliderTokens.kt (Jetpack Compose M3 Expressive)
5
+ * - M3 Expressive visual spec (handle squeeze, gaps, asymmetric radii)
6
+ *
7
+ * All dimensional values are in px (dp equivalents for web).
8
+ * Colors reference CSS custom properties — do NOT hardcode hex.
9
+ * @see docs/m3/sliders/Slider.kt
10
+ */
11
+ /**
12
+ * Core dimensional design tokens for the MD3 Expressive Slider.
13
+ * Maps directly from SliderTokens.kt to CSS/JS values.
14
+ */
15
+ export declare const SliderTokens: {
16
+ readonly trackSizes: {
17
+ xs: number;
18
+ s: number;
19
+ m: number;
20
+ l: number;
21
+ xl: number;
22
+ };
23
+ readonly trackShapes: {
24
+ xs: number;
25
+ s: number;
26
+ m: number;
27
+ l: number;
28
+ xl: number;
29
+ };
30
+ /** Thumb height grows with track size */
31
+ readonly thumbHeights: {
32
+ xs: number;
33
+ s: number;
34
+ m: number;
35
+ l: number;
36
+ xl: number;
37
+ };
38
+ /**
39
+ * Thumb width at rest and on hover.
40
+ * MD3 Expressive handle pill: 4dp wide.
41
+ */
42
+ readonly thumbWidthDefault: 4;
43
+ /**
44
+ * Thumb width while pressed/dragging.
45
+ * "Squeeze" animation: 4dp → 2dp (M3 Expressive behavior).
46
+ */
47
+ readonly thumbWidthPressed: 2;
48
+ /**
49
+ * Transparent gap between the active/inactive track segments and the thumb.
50
+ * Maps to ActiveHandleLeadingSpace / ActiveHandleTrailingSpace = 6dp.
51
+ *
52
+ * This gap is rendered by mathematically subtracting it from the track
53
+ * segment width — NOT using margin/padding.
54
+ */
55
+ readonly thumbGap: 6;
56
+ /**
57
+ * Corner radius on the INNER ends of each track segment (facing the thumb).
58
+ * Fixed to 2px according to MD3 Expressive Slider specs (TrackInsideCornerSize).
59
+ * Outer ends use `trackSize / 2` (pill cap) — computed inline per component.
60
+ */
61
+ readonly trackInnerRadius: 2;
62
+ /** Tick dot size (width and height). = 4dp. */
63
+ readonly tickSize: 4;
64
+ /**
65
+ * Minimum touch target for the thumb wrapper.
66
+ * MD3 requires 48dp minimum touch target for interactive elements.
67
+ */
68
+ readonly thumbTouchTarget: 48;
69
+ /** Offset above the thumb center for the value indicator tooltip. */
70
+ readonly valueIndicatorOffset: 52;
71
+ /** Standard icon size map for inset icons inside the track. */
72
+ readonly insetIconSizes: {
73
+ xs: number;
74
+ s: number;
75
+ m: number;
76
+ l: number;
77
+ xl: number;
78
+ };
79
+ /**
80
+ * Padding between icon and track edge (horizontal).
81
+ * Keeps the icon visually centered within the pill track.
82
+ */
83
+ readonly insetIconPadding: 4;
84
+ };
85
+ /**
86
+ * CSS custom property references for Slider colors.
87
+ * Maps to --md-sys-color-* tokens in the MD3 theme system.
88
+ *
89
+ * DO NOT hardcode hex/rgba values — these references automatically
90
+ * adapt to light/dark theme.
91
+ */
92
+ export declare const SliderColors: {
93
+ /** Active track color. Maps to ActiveTrackColor token. */
94
+ readonly activeTrack: "var(--md-sys-color-primary)";
95
+ /** Inactive track color. Maps to InactiveTrackColor token. */
96
+ readonly inactiveTrack: "var(--md-sys-color-secondary-container)";
97
+ /**
98
+ * Disabled track color.
99
+ * Uses opacity + on-surface per MD3 disabled state spec.
100
+ */
101
+ readonly disabledActiveTrack: "var(--md-sys-color-on-surface)";
102
+ readonly disabledInactiveTrack: "var(--md-sys-color-on-surface)";
103
+ /** Thumb (handle) color. Maps to HandleColor token. */
104
+ readonly thumb: "var(--md-sys-color-primary)";
105
+ /** Disabled thumb color. */
106
+ readonly disabledThumb: "var(--md-sys-color-on-surface)";
107
+ /** Value indicator (tooltip) background. Maps to InverseSurface. */
108
+ readonly valueIndicatorBg: "var(--md-sys-color-inverse-surface)";
109
+ /** Value indicator (tooltip) text. Maps to InverseOnSurface. */
110
+ readonly valueIndicatorText: "var(--md-sys-color-inverse-on-surface)";
111
+ /**
112
+ * Tick on the INACTIVE portion of the track.
113
+ * Maps to TickMarkActiveContainerColor (in spec: primary color stands out).
114
+ */
115
+ readonly tickOnInactive: "var(--md-sys-color-primary)";
116
+ /**
117
+ * Tick on the ACTIVE portion of the track.
118
+ * Maps to TickMarkInactiveContainerColor (secondary-container blends in).
119
+ */
120
+ readonly tickOnActive: "var(--md-sys-color-secondary-container)";
121
+ readonly disabledTick: "var(--md-sys-color-on-surface)";
122
+ };
123
+ /**
124
+ * FastSpatial spring for thumb width squeeze animation.
125
+ * Equivalent to MotionSchemeKeyTokens.FastSpatial in Compose.
126
+ */
127
+ export declare const SLIDER_THUMB_SPRING: {
128
+ readonly type: "spring";
129
+ readonly stiffness: 500;
130
+ readonly damping: 40;
131
+ };
132
+ /**
133
+ * DefaultSpatial spring for thumb position/track updates.
134
+ * Slightly looser feel for position changes.
135
+ */
136
+ export declare const SLIDER_POSITION_SPRING: {
137
+ readonly type: "spring";
138
+ readonly stiffness: 400;
139
+ readonly damping: 38;
140
+ };
141
+ /** Color crossfade for active/inactive track color transitions. */
142
+ export declare const SLIDER_COLOR_TRANSITION: {
143
+ readonly duration: 0.18;
144
+ readonly ease: "easeInOut";
145
+ };
146
+ /** Value indicator appear/disappear animation. */
147
+ export declare const SLIDER_INDICATOR_TRANSITION: {
148
+ readonly type: "spring";
149
+ readonly stiffness: 450;
150
+ readonly damping: 32;
151
+ };