@mrmeg/expo-ui 0.6.1 → 0.7.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/LLM_USAGE.md +4 -5
- package/README.md +6 -6
- package/dist/components/Accordion.js +21 -16
- package/dist/components/AnimatedView.d.ts +1 -1
- package/dist/components/AnimatedView.js +2 -2
- package/dist/components/Badge.d.ts +3 -2
- package/dist/components/Badge.js +4 -3
- package/dist/components/BottomSheet.js +31 -29
- package/dist/components/BottomSheetKeyboard.d.ts +7 -0
- package/dist/components/BottomSheetKeyboard.js +35 -0
- package/dist/components/Button.d.ts +55 -13
- package/dist/components/Button.js +72 -28
- package/dist/components/Card.js +8 -10
- package/dist/components/Checkbox.js +22 -25
- package/dist/components/Collapsible.js +3 -7
- package/dist/components/Dialog.js +1 -1
- package/dist/components/DismissKeyboard.js +3 -3
- package/dist/components/Drawer.js +21 -10
- package/dist/components/DropdownMenu.d.ts +3 -2
- package/dist/components/DropdownMenu.js +29 -29
- package/dist/components/EmptyState.js +1 -1
- package/dist/components/InputOTP.js +16 -40
- package/dist/components/Notification.js +50 -25
- package/dist/components/Popover.js +1 -1
- package/dist/components/Progress.d.ts +2 -2
- package/dist/components/Progress.js +36 -34
- package/dist/components/RadioGroup.js +22 -20
- package/dist/components/Select.js +30 -20
- package/dist/components/Skeleton.js +6 -6
- package/dist/components/Slider.js +90 -97
- package/dist/components/StyledText.context.d.ts +6 -0
- package/dist/components/StyledText.context.js +5 -0
- package/dist/components/StyledText.d.ts +7 -58
- package/dist/components/StyledText.js +8 -28
- package/dist/components/Switch.js +30 -26
- package/dist/components/Tabs.d.ts +23 -3
- package/dist/components/Tabs.js +39 -17
- package/dist/components/TextInput.d.ts +6 -2
- package/dist/components/TextInput.js +6 -7
- package/dist/components/Toggle.js +12 -7
- package/dist/components/ToggleGroup.js +17 -11
- package/dist/components/Tooltip.js +1 -1
- package/dist/hooks/useDimensions.js +25 -26
- package/dist/hooks/useReduceMotion.d.ts +5 -1
- package/dist/hooks/useReduceMotion.js +46 -41
- package/dist/hooks/useResources.js +6 -1
- package/dist/hooks/useScalePress.d.ts +6 -5
- package/dist/hooks/useScalePress.js +25 -21
- package/dist/hooks/useStaggeredEntrance.d.ts +9 -8
- package/dist/hooks/useStaggeredEntrance.js +48 -21
- package/dist/state/themeColorScope.js +3 -3
- package/llms-full.md +4 -5
- package/llms.txt +2 -2
- package/package.json +1 -4
|
@@ -15,8 +15,8 @@ export interface ProgressProps {
|
|
|
15
15
|
* Progress Component
|
|
16
16
|
*
|
|
17
17
|
* A linear progress bar supporting determinate and indeterminate modes.
|
|
18
|
-
* Determinate mode animates the fill
|
|
19
|
-
* Indeterminate mode pulses opacity via
|
|
18
|
+
* Determinate mode animates the fill with React Native Animated.
|
|
19
|
+
* Indeterminate mode pulses opacity via Animated.loop.
|
|
20
20
|
*
|
|
21
21
|
* @example
|
|
22
22
|
* ```tsx
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
3
|
import { Animated, StyleSheet, View } from "react-native";
|
|
4
|
-
import Reanimated, { useSharedValue, useAnimatedStyle, withTiming, useReducedMotion, } from "react-native-reanimated";
|
|
5
4
|
import { useTheme } from "../hooks/useTheme.js";
|
|
6
|
-
import {
|
|
5
|
+
import { useReducedMotion } from "../hooks/useReduceMotion.js";
|
|
7
6
|
// ============================================================================
|
|
8
7
|
// Constants
|
|
9
8
|
// ============================================================================
|
|
@@ -19,8 +18,8 @@ const SIZE_MAP = {
|
|
|
19
18
|
* Progress Component
|
|
20
19
|
*
|
|
21
20
|
* A linear progress bar supporting determinate and indeterminate modes.
|
|
22
|
-
* Determinate mode animates the fill
|
|
23
|
-
* Indeterminate mode pulses opacity via
|
|
21
|
+
* Determinate mode animates the fill with React Native Animated.
|
|
22
|
+
* Indeterminate mode pulses opacity via Animated.loop.
|
|
24
23
|
*
|
|
25
24
|
* @example
|
|
26
25
|
* ```tsx
|
|
@@ -52,28 +51,28 @@ export function Progress({ value, variant = "default", size = "md", style, }) {
|
|
|
52
51
|
]), children: isDeterminate ? (_jsx(DeterminateFill, { value: value, fillColor: fillColor, height: height, borderRadius: borderRadius, reduceMotion: reduceMotion })) : (_jsx(IndeterminateFill, { fillColor: fillColor, height: height, borderRadius: borderRadius, reduceMotion: reduceMotion })) }));
|
|
53
52
|
}
|
|
54
53
|
function DeterminateFill({ value, fillColor, height, borderRadius, reduceMotion, }) {
|
|
55
|
-
const [containerWidth, setContainerWidth] = useState(0);
|
|
56
54
|
const clamped = Math.min(100, Math.max(0, value));
|
|
57
|
-
|
|
55
|
+
// Animate scaleX (GPU compositor) instead of width (JS-thread layout each
|
|
56
|
+
// frame). transformOrigin "left" grows the fill from the left edge, so a
|
|
57
|
+
// full-width bar scaled by clamped/100 needs no container measurement.
|
|
58
|
+
const scaleX = useRef(new Animated.Value(0)).current;
|
|
58
59
|
useEffect(() => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const target = (clamped / 100) * containerWidth;
|
|
62
|
-
animatedWidth.value = withTiming(target, {
|
|
60
|
+
Animated.timing(scaleX, {
|
|
61
|
+
toValue: clamped / 100,
|
|
63
62
|
duration: reduceMotion ? 0 : 300,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
63
|
+
useNativeDriver: true,
|
|
64
|
+
}).start();
|
|
65
|
+
}, [clamped, reduceMotion, scaleX]);
|
|
66
|
+
return (_jsx(Animated.View, { style: [
|
|
67
|
+
{
|
|
68
|
+
width: "100%",
|
|
69
|
+
height,
|
|
70
|
+
borderRadius,
|
|
71
|
+
backgroundColor: fillColor,
|
|
72
|
+
transformOrigin: "left",
|
|
73
|
+
},
|
|
74
|
+
{ transform: [{ scaleX }] },
|
|
75
|
+
] }));
|
|
77
76
|
}
|
|
78
77
|
function IndeterminateFill({ fillColor, height, borderRadius, reduceMotion, }) {
|
|
79
78
|
const opacity = useRef(new Animated.Value(reduceMotion ? 0.7 : 0.4)).current;
|
|
@@ -82,26 +81,29 @@ function IndeterminateFill({ fillColor, height, borderRadius, reduceMotion, }) {
|
|
|
82
81
|
opacity.setValue(0.7);
|
|
83
82
|
return;
|
|
84
83
|
}
|
|
84
|
+
opacity.setValue(0.4);
|
|
85
85
|
const animation = Animated.loop(Animated.sequence([
|
|
86
86
|
Animated.timing(opacity, {
|
|
87
|
-
toValue: 1
|
|
87
|
+
toValue: 1,
|
|
88
88
|
duration: 800,
|
|
89
|
-
useNativeDriver:
|
|
89
|
+
useNativeDriver: true,
|
|
90
90
|
}),
|
|
91
91
|
Animated.timing(opacity, {
|
|
92
92
|
toValue: 0.4,
|
|
93
93
|
duration: 800,
|
|
94
|
-
useNativeDriver:
|
|
94
|
+
useNativeDriver: true,
|
|
95
95
|
}),
|
|
96
96
|
]));
|
|
97
97
|
animation.start();
|
|
98
98
|
return () => animation.stop();
|
|
99
99
|
}, [opacity, reduceMotion]);
|
|
100
|
-
return (_jsx(Animated.View, { style:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
100
|
+
return (_jsx(Animated.View, { style: [
|
|
101
|
+
{
|
|
102
|
+
width: "40%",
|
|
103
|
+
height,
|
|
104
|
+
borderRadius,
|
|
105
|
+
backgroundColor: fillColor,
|
|
106
|
+
},
|
|
107
|
+
{ opacity },
|
|
108
|
+
] }));
|
|
107
109
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { createContext,
|
|
3
|
-
import { View, StyleSheet, Pressable, Platform } from "react-native";
|
|
4
|
-
import Animated, { useSharedValue, useAnimatedStyle, withTiming, useReducedMotion, } from "react-native-reanimated";
|
|
2
|
+
import React, { createContext, use, useEffect, useRef } from "react";
|
|
3
|
+
import { View, StyleSheet, Pressable, Platform, Animated } from "react-native";
|
|
5
4
|
import { StyledText } from "./StyledText.js";
|
|
6
5
|
import { useTheme } from "../hooks/useTheme.js";
|
|
7
6
|
import { spacing } from "../constants/spacing.js";
|
|
8
7
|
import { hapticLight } from "../lib/haptics.js";
|
|
8
|
+
import { useReducedMotion } from "../hooks/useReduceMotion.js";
|
|
9
9
|
import * as RadioGroupPrimitive from "@rn-primitives/radio-group";
|
|
10
10
|
const DEFAULT_HIT_SLOP = 8;
|
|
11
11
|
const SIZE_CONFIGS = {
|
|
@@ -14,8 +14,11 @@ const SIZE_CONFIGS = {
|
|
|
14
14
|
lg: { outer: 24, inner: 12, borderWidth: 1 },
|
|
15
15
|
};
|
|
16
16
|
const RadioGroupContext = createContext(null);
|
|
17
|
+
function handleRadioPress() {
|
|
18
|
+
hapticLight();
|
|
19
|
+
}
|
|
17
20
|
function useRadioGroupContext() {
|
|
18
|
-
const context =
|
|
21
|
+
const context = use(RadioGroupContext);
|
|
19
22
|
if (context === null) {
|
|
20
23
|
throw new Error("RadioGroup compound components cannot be rendered outside the RadioGroup component");
|
|
21
24
|
}
|
|
@@ -38,7 +41,8 @@ function useRadioGroupContext() {
|
|
|
38
41
|
*/
|
|
39
42
|
function RadioGroupRoot({ size = "md", error = false, value, onValueChange, style: styleOverride, children, ...props }) {
|
|
40
43
|
const flattenedStyle = styleOverride ? StyleSheet.flatten(styleOverride) : undefined;
|
|
41
|
-
|
|
44
|
+
const contextValue = React.useMemo(() => ({ size, error, value, onValueChange }), [size, error, value, onValueChange]);
|
|
45
|
+
return (_jsx(RadioGroupContext.Provider, { value: contextValue, children: _jsx(RadioGroupPrimitive.Root, { ...props, value: value, onValueChange: onValueChange, style: {
|
|
42
46
|
flexDirection: "column",
|
|
43
47
|
gap: spacing.listItemSpacing,
|
|
44
48
|
...(flattenedStyle || {}),
|
|
@@ -56,19 +60,14 @@ function RadioGroupItem({ label, required = false, style: styleOverride, labelSt
|
|
|
56
60
|
const sizeConfig = SIZE_CONFIGS[size];
|
|
57
61
|
const isChecked = groupValue === itemValue;
|
|
58
62
|
// Animated dot scale — follows Checkbox opacity pattern
|
|
59
|
-
const dotScale =
|
|
63
|
+
const dotScale = useRef(new Animated.Value(isChecked ? 1 : 0)).current;
|
|
60
64
|
useEffect(() => {
|
|
61
|
-
dotScale
|
|
62
|
-
|
|
63
|
-
:
|
|
65
|
+
Animated.timing(dotScale, {
|
|
66
|
+
toValue: isChecked ? 1 : 0,
|
|
67
|
+
duration: reduceMotion ? 0 : 60,
|
|
68
|
+
useNativeDriver: true,
|
|
69
|
+
}).start();
|
|
64
70
|
}, [isChecked, reduceMotion, dotScale]);
|
|
65
|
-
const dotStyle = useAnimatedStyle(() => ({
|
|
66
|
-
transform: [{ scale: dotScale.value }],
|
|
67
|
-
}));
|
|
68
|
-
// Wrap onPress to add haptic feedback
|
|
69
|
-
const handlePress = () => {
|
|
70
|
-
hapticLight();
|
|
71
|
-
};
|
|
72
71
|
// Border color follows Checkbox pattern
|
|
73
72
|
const borderColor = error
|
|
74
73
|
? theme.colors.destructive
|
|
@@ -76,20 +75,19 @@ function RadioGroupItem({ label, required = false, style: styleOverride, labelSt
|
|
|
76
75
|
? theme.colors.primary
|
|
77
76
|
: getContrastingColor(theme.colors.background, theme.colors.text, theme.colors.textDim);
|
|
78
77
|
const flattenedStyle = styleOverride ? StyleSheet.flatten(styleOverride) : undefined;
|
|
79
|
-
const radioElement = (_jsx(RadioGroupPrimitive.Item, { ...props, value: itemValue, disabled: disabled, onPress:
|
|
78
|
+
const radioElement = (_jsx(RadioGroupPrimitive.Item, { ...props, value: itemValue, disabled: disabled, onPress: handleRadioPress, style: {
|
|
79
|
+
...styles.radio,
|
|
80
80
|
borderColor,
|
|
81
81
|
backgroundColor: theme.colors.background,
|
|
82
82
|
borderRadius: sizeConfig.outer / 2,
|
|
83
83
|
borderWidth: sizeConfig.borderWidth,
|
|
84
84
|
width: sizeConfig.outer,
|
|
85
85
|
height: sizeConfig.outer,
|
|
86
|
-
justifyContent: "center",
|
|
87
|
-
alignItems: "center",
|
|
88
86
|
opacity: disabled ? 0.5 : 1,
|
|
89
87
|
...(Platform.OS === "web" && { cursor: disabled ? "not-allowed" : "pointer" }),
|
|
90
88
|
...(flattenedStyle || {}),
|
|
91
89
|
}, hitSlop: DEFAULT_HIT_SLOP, accessibilityLabel: label, children: _jsx(Animated.View, { style: [
|
|
92
|
-
|
|
90
|
+
{ transform: [{ scale: dotScale }] },
|
|
93
91
|
{
|
|
94
92
|
width: sizeConfig.inner,
|
|
95
93
|
height: sizeConfig.inner,
|
|
@@ -118,6 +116,10 @@ function RadioGroupItem({ label, required = false, style: styleOverride, labelSt
|
|
|
118
116
|
], children: [label, required && (_jsx(StyledText, { selectable: false, style: [styles.required, { color: theme.colors.destructive }], children: " *" }))] }) })] }));
|
|
119
117
|
}
|
|
120
118
|
const styles = StyleSheet.create({
|
|
119
|
+
radio: {
|
|
120
|
+
justifyContent: "center",
|
|
121
|
+
alignItems: "center",
|
|
122
|
+
},
|
|
121
123
|
container: {
|
|
122
124
|
flexDirection: "row",
|
|
123
125
|
alignItems: "center",
|
|
@@ -3,7 +3,7 @@ import * as React from "react";
|
|
|
3
3
|
import { Platform, StyleSheet, View } from "react-native";
|
|
4
4
|
import { Icon } from "./Icon.js";
|
|
5
5
|
import { AnimatedView } from "./AnimatedView.js";
|
|
6
|
-
import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.
|
|
6
|
+
import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.context";
|
|
7
7
|
import { useTheme } from "../hooks/useTheme.js";
|
|
8
8
|
import { spacing } from "../constants/spacing.js";
|
|
9
9
|
import * as SelectPrimitive from "@rn-primitives/select";
|
|
@@ -37,14 +37,10 @@ function SelectTrigger({ size = "md", error = false, children, style: styleOverr
|
|
|
37
37
|
const { theme } = useTheme();
|
|
38
38
|
const sizeConfig = SIZE_CONFIGS[size];
|
|
39
39
|
return (_jsx(SelectPrimitive.Trigger, { disabled: disabled, ...props, style: {
|
|
40
|
-
|
|
41
|
-
justifyContent: "space-between",
|
|
42
|
-
alignItems: "center",
|
|
40
|
+
...styles.trigger,
|
|
43
41
|
height: sizeConfig.height,
|
|
44
42
|
paddingHorizontal: sizeConfig.paddingHorizontal,
|
|
45
|
-
borderWidth: 1,
|
|
46
43
|
borderColor: error ? theme.colors.destructive : theme.colors.border,
|
|
47
|
-
borderRadius: spacing.radiusMd,
|
|
48
44
|
backgroundColor: theme.colors.background,
|
|
49
45
|
...(Platform.OS === "web" && {
|
|
50
46
|
cursor: disabled ? "not-allowed" : "pointer",
|
|
@@ -98,19 +94,13 @@ function SelectContent({ side, align = "start", sideOffset = 4, portalHost, styl
|
|
|
98
94
|
}
|
|
99
95
|
function SelectItem({ children, style: styleOverride, ...props }) {
|
|
100
96
|
const { theme } = useTheme();
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
97
|
+
// Render custom element/array children when provided; otherwise fall back to
|
|
98
|
+
// the default ItemText driven by the required `label` prop. Discriminating on
|
|
99
|
+
// "is this a renderable node?" keeps the API explicit without switching on the
|
|
100
|
+
// primitive type of children.
|
|
101
|
+
const hasCustomChildren = React.isValidElement(children) || Array.isArray(children);
|
|
104
102
|
return (_jsx(TextClassContext.Provider, { value: "", children: _jsxs(SelectPrimitive.Item, { ...props, style: {
|
|
105
|
-
|
|
106
|
-
flexDirection: "row",
|
|
107
|
-
alignItems: "center",
|
|
108
|
-
gap: spacing.sm,
|
|
109
|
-
borderRadius: spacing.radiusSm,
|
|
110
|
-
paddingVertical: Platform.select({ web: spacing.xs, default: spacing.sm }),
|
|
111
|
-
paddingLeft: spacing.xl,
|
|
112
|
-
paddingRight: spacing.sm,
|
|
113
|
-
backgroundColor: "transparent",
|
|
103
|
+
...styles.item,
|
|
114
104
|
...(Platform.OS === "web" && {
|
|
115
105
|
cursor: props.disabled ? "not-allowed" : "pointer",
|
|
116
106
|
outlineStyle: "none",
|
|
@@ -127,11 +117,11 @@ function SelectItem({ children, style: styleOverride, ...props }) {
|
|
|
127
117
|
width: 14,
|
|
128
118
|
alignItems: "center",
|
|
129
119
|
justifyContent: "center",
|
|
130
|
-
}, children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(Icon, { name: "check", size: 16, color: theme.colors.accent, ...(Platform.OS === "web" && { style: { pointerEvents: "none" } }) }) }) }), _jsx(TextSelectabilityContext.Provider, { value: false, children:
|
|
120
|
+
}, children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(Icon, { name: "check", size: 16, color: theme.colors.accent, ...(Platform.OS === "web" && { style: { pointerEvents: "none" } }) }) }) }), _jsx(TextSelectabilityContext.Provider, { value: false, children: hasCustomChildren ? (children) : (_jsx(SelectPrimitive.ItemText, { style: {
|
|
131
121
|
color: theme.colors.popoverForeground,
|
|
132
122
|
fontSize: 14,
|
|
133
123
|
lineHeight: 20,
|
|
134
|
-
} }))
|
|
124
|
+
} })) })] }) }));
|
|
135
125
|
}
|
|
136
126
|
function SelectGroup({ style: styleOverride, ...props }) {
|
|
137
127
|
return (_jsx(SelectPrimitive.Group, { ...props, style: {
|
|
@@ -166,6 +156,26 @@ function SelectSeparator({ style: styleOverride, ...props }) {
|
|
|
166
156
|
: {}),
|
|
167
157
|
} }));
|
|
168
158
|
}
|
|
159
|
+
const styles = StyleSheet.create({
|
|
160
|
+
trigger: {
|
|
161
|
+
flexDirection: "row",
|
|
162
|
+
justifyContent: "space-between",
|
|
163
|
+
alignItems: "center",
|
|
164
|
+
borderWidth: 1,
|
|
165
|
+
borderRadius: spacing.radiusMd,
|
|
166
|
+
},
|
|
167
|
+
item: {
|
|
168
|
+
position: "relative",
|
|
169
|
+
flexDirection: "row",
|
|
170
|
+
alignItems: "center",
|
|
171
|
+
gap: spacing.sm,
|
|
172
|
+
borderRadius: spacing.radiusSm,
|
|
173
|
+
paddingVertical: Platform.select({ web: spacing.xs, default: spacing.sm }),
|
|
174
|
+
paddingLeft: spacing.xl,
|
|
175
|
+
paddingRight: spacing.sm,
|
|
176
|
+
backgroundColor: "transparent",
|
|
177
|
+
},
|
|
178
|
+
});
|
|
169
179
|
/**
|
|
170
180
|
* Select Component with Sub-components
|
|
171
181
|
* Properly typed interface for dot notation access (e.g., Select.Trigger)
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef } from "react";
|
|
3
|
-
import { View,
|
|
4
|
-
import { useReducedMotion } from "react-native-reanimated";
|
|
3
|
+
import { View, StyleSheet, Animated } from "react-native";
|
|
5
4
|
import { useTheme } from "../hooks/useTheme.js";
|
|
6
|
-
import {
|
|
5
|
+
import { useReducedMotion } from "../hooks/useReduceMotion.js";
|
|
7
6
|
import { spacing } from "../constants/spacing.js";
|
|
8
7
|
/**
|
|
9
8
|
* Skeleton Component
|
|
@@ -26,16 +25,17 @@ export function Skeleton({ width = "100%", height = 20, borderRadius = spacing.r
|
|
|
26
25
|
opacity.setValue(0.6);
|
|
27
26
|
return;
|
|
28
27
|
}
|
|
28
|
+
opacity.setValue(0.3);
|
|
29
29
|
const animation = Animated.loop(Animated.sequence([
|
|
30
30
|
Animated.timing(opacity, {
|
|
31
31
|
toValue: 1,
|
|
32
32
|
duration: 800,
|
|
33
|
-
useNativeDriver:
|
|
33
|
+
useNativeDriver: true,
|
|
34
34
|
}),
|
|
35
35
|
Animated.timing(opacity, {
|
|
36
36
|
toValue: 0.3,
|
|
37
37
|
duration: 800,
|
|
38
|
-
useNativeDriver:
|
|
38
|
+
useNativeDriver: true,
|
|
39
39
|
}),
|
|
40
40
|
]));
|
|
41
41
|
animation.start();
|
|
@@ -48,8 +48,8 @@ export function Skeleton({ width = "100%", height = 20, borderRadius = spacing.r
|
|
|
48
48
|
height: circle ? resolvedSize : height,
|
|
49
49
|
borderRadius: circle ? (resolvedSize / 2) : borderRadius,
|
|
50
50
|
backgroundColor: theme.colors.muted,
|
|
51
|
-
opacity,
|
|
52
51
|
},
|
|
52
|
+
{ opacity },
|
|
53
53
|
style,
|
|
54
54
|
] }));
|
|
55
55
|
}
|
|
@@ -2,17 +2,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { palette } from "../constants/colors.js";
|
|
3
3
|
import { useTheme } from "../hooks/useTheme.js";
|
|
4
4
|
import { hapticLight } from "../lib/haptics.js";
|
|
5
|
-
import { useCallback, useEffect, useRef } from "react";
|
|
6
|
-
import { Platform, StyleSheet, View } from "react-native";
|
|
7
|
-
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
8
|
-
import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming, } from "react-native-reanimated";
|
|
5
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
+
import { Animated, PanResponder, Platform, StyleSheet, View } from "react-native";
|
|
9
7
|
import { StyledText } from "./StyledText.js";
|
|
10
8
|
const SIZES = {
|
|
11
9
|
sm: { track: 4, thumb: 16 },
|
|
12
10
|
md: { track: 6, thumb: 20 },
|
|
13
11
|
};
|
|
14
12
|
function clampAndSnap(raw, min, max, step) {
|
|
15
|
-
"worklet";
|
|
16
13
|
const clamped = Math.min(Math.max(raw, min), max);
|
|
17
14
|
const stepped = Math.round((clamped - min) / step) * step + min;
|
|
18
15
|
// Avoid floating-point drift
|
|
@@ -37,76 +34,70 @@ function Slider({ value = 0, onValueChange, min = 0, max = 100, step = 1, size =
|
|
|
37
34
|
? withAlpha(palette.white, 0.32)
|
|
38
35
|
: theme.colors.mutedForeground
|
|
39
36
|
: theme.colors.accent;
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
37
|
+
const [trackWidth, setTrackWidth] = useState(0);
|
|
38
|
+
const trackWidthRef = useRef(0);
|
|
39
|
+
const thumbX = useRef(new Animated.Value(0)).current;
|
|
40
|
+
const lastSnappedValue = useRef(value);
|
|
41
|
+
const updateFromPosition = useCallback((rawX) => {
|
|
42
|
+
const width = trackWidthRef.current;
|
|
43
|
+
const x = Math.min(Math.max(rawX, 0), width);
|
|
44
|
+
thumbX.stopAnimation();
|
|
45
|
+
thumbX.setValue(x);
|
|
46
|
+
const ratio = width > 0 ? x / width : 0;
|
|
47
|
+
const raw = min + ratio * (max - min);
|
|
48
|
+
const snapped = clampAndSnap(raw, min, max, step);
|
|
49
|
+
if (snapped !== lastSnappedValue.current) {
|
|
50
|
+
lastSnappedValue.current = snapped;
|
|
51
|
+
hapticLight();
|
|
52
|
+
}
|
|
53
|
+
onValueChange?.(snapped);
|
|
54
|
+
}, [max, min, onValueChange, step, thumbX]);
|
|
55
|
+
const handleGesture = useCallback((event) => {
|
|
56
|
+
updateFromPosition(event.nativeEvent.locationX);
|
|
57
|
+
}, [updateFromPosition]);
|
|
58
|
+
const panResponder = useMemo(() => PanResponder.create({
|
|
59
|
+
onStartShouldSetPanResponder: () => !disabled,
|
|
60
|
+
onMoveShouldSetPanResponder: () => !disabled,
|
|
61
|
+
onPanResponderGrant: handleGesture,
|
|
62
|
+
onPanResponderMove: handleGesture,
|
|
63
|
+
}), [disabled, handleGesture]);
|
|
57
64
|
useEffect(() => {
|
|
58
65
|
const ratio = getValueRatio(value, min, max);
|
|
59
|
-
const width =
|
|
66
|
+
const width = trackWidthRef.current;
|
|
60
67
|
if (width > 0) {
|
|
61
|
-
|
|
68
|
+
Animated.timing(thumbX, {
|
|
69
|
+
toValue: ratio * width,
|
|
70
|
+
duration: 80,
|
|
71
|
+
useNativeDriver: true,
|
|
72
|
+
}).start();
|
|
62
73
|
}
|
|
63
|
-
lastSnappedValue.
|
|
64
|
-
}, [
|
|
74
|
+
lastSnappedValue.current = value;
|
|
75
|
+
}, [max, min, thumbX, value]);
|
|
65
76
|
const onTrackLayout = useCallback((e) => {
|
|
66
77
|
const w = e.nativeEvent.layout.width;
|
|
67
|
-
|
|
78
|
+
trackWidthRef.current = w;
|
|
79
|
+
setTrackWidth(w);
|
|
68
80
|
// Set initial thumb position without animation
|
|
69
81
|
const ratio = getValueRatio(value, min, max);
|
|
70
|
-
thumbX.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
.onUpdate((e) => {
|
|
89
|
-
"worklet";
|
|
90
|
-
const x = Math.min(Math.max(e.x, 0), trackWidth.value);
|
|
91
|
-
thumbX.value = x;
|
|
92
|
-
const ratio = trackWidth.value > 0 ? x / trackWidth.value : 0;
|
|
93
|
-
const raw = min + ratio * (max - min);
|
|
94
|
-
const snapped = clampAndSnap(raw, min, max, step);
|
|
95
|
-
if (snapped !== lastSnappedValue.value) {
|
|
96
|
-
lastSnappedValue.value = snapped;
|
|
97
|
-
runOnJS(jsHaptic)();
|
|
98
|
-
}
|
|
99
|
-
runOnJS(jsOnValueChange)(snapped);
|
|
82
|
+
thumbX.stopAnimation();
|
|
83
|
+
thumbX.setValue(ratio * w);
|
|
84
|
+
}, [max, min, thumbX, value]);
|
|
85
|
+
const safeTrackWidth = Math.max(trackWidth, 1);
|
|
86
|
+
const fillScale = thumbX.interpolate({
|
|
87
|
+
inputRange: [0, safeTrackWidth],
|
|
88
|
+
outputRange: [0, 1],
|
|
89
|
+
extrapolate: "clamp",
|
|
90
|
+
});
|
|
91
|
+
const thumbTranslateX = thumbX.interpolate({
|
|
92
|
+
inputRange: [0, safeTrackWidth],
|
|
93
|
+
outputRange: [-dims.thumb / 2, safeTrackWidth - dims.thumb / 2],
|
|
94
|
+
extrapolate: "clamp",
|
|
95
|
+
});
|
|
96
|
+
const labelTranslateX = thumbX.interpolate({
|
|
97
|
+
inputRange: [0, safeTrackWidth],
|
|
98
|
+
outputRange: [-14, safeTrackWidth - 14],
|
|
99
|
+
extrapolate: "clamp",
|
|
100
100
|
});
|
|
101
|
-
const fillStyle = useAnimatedStyle(() => ({
|
|
102
|
-
width: thumbX.value,
|
|
103
|
-
}));
|
|
104
|
-
const thumbAnimatedStyle = useAnimatedStyle(() => ({
|
|
105
|
-
transform: [{ translateX: thumbX.value - dims.thumb / 2 }],
|
|
106
|
-
}));
|
|
107
|
-
const valueLabelStyle = useAnimatedStyle(() => ({
|
|
108
|
-
transform: [{ translateX: thumbX.value - 14 }],
|
|
109
|
-
}));
|
|
110
101
|
const flattenedStyle = styleOverride ? StyleSheet.flatten(styleOverride) : undefined;
|
|
111
102
|
// Accessibility action handler
|
|
112
103
|
const handleAccessibilityAction = useCallback((event) => {
|
|
@@ -132,42 +123,44 @@ function Slider({ value = 0, onValueChange, min = 0, max = 100, step = 1, size =
|
|
|
132
123
|
width: 28,
|
|
133
124
|
alignItems: "center",
|
|
134
125
|
},
|
|
135
|
-
|
|
126
|
+
{ transform: [{ translateX: labelTranslateX }] },
|
|
136
127
|
{ pointerEvents: "none" },
|
|
137
128
|
], children: _jsx(StyledText, { selectable: false, style: {
|
|
138
|
-
fontSize:
|
|
129
|
+
fontSize: 12,
|
|
139
130
|
color: theme.colors.textDim,
|
|
140
131
|
userSelect: "none",
|
|
141
|
-
}, children: value }) })),
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
{
|
|
152
|
-
height: dims.track,
|
|
153
|
-
borderRadius: dims.track / 2,
|
|
154
|
-
backgroundColor: activeTrackColor,
|
|
155
|
-
},
|
|
156
|
-
fillStyle,
|
|
157
|
-
] }) }), _jsx(Animated.View, { style: [
|
|
132
|
+
}, children: value }) })), _jsxs(View, { style: {
|
|
133
|
+
height: dims.thumb,
|
|
134
|
+
justifyContent: "center",
|
|
135
|
+
...(Platform.OS === "web" && { cursor: disabled ? "default" : "pointer" }),
|
|
136
|
+
}, onLayout: onTrackLayout, ...panResponder.panHandlers, children: [_jsx(View, { style: {
|
|
137
|
+
height: dims.track,
|
|
138
|
+
borderRadius: dims.track / 2,
|
|
139
|
+
backgroundColor: inactiveTrackColor,
|
|
140
|
+
overflow: "hidden",
|
|
141
|
+
}, children: _jsx(Animated.View, { style: [
|
|
158
142
|
{
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
borderRadius: dims.thumb / 2,
|
|
165
|
-
backgroundColor: thumbBackgroundColor,
|
|
166
|
-
borderWidth: 1,
|
|
167
|
-
borderColor: thumbBorderColor,
|
|
168
|
-
...getShadowStyle("subtle"),
|
|
143
|
+
width: "100%",
|
|
144
|
+
height: dims.track,
|
|
145
|
+
borderRadius: dims.track / 2,
|
|
146
|
+
backgroundColor: activeTrackColor,
|
|
147
|
+
transformOrigin: "left",
|
|
169
148
|
},
|
|
170
|
-
|
|
171
|
-
] })
|
|
149
|
+
{ transform: [{ scaleX: fillScale }] },
|
|
150
|
+
] }) }), _jsx(Animated.View, { style: [
|
|
151
|
+
{
|
|
152
|
+
position: "absolute",
|
|
153
|
+
top: 0,
|
|
154
|
+
left: 0,
|
|
155
|
+
width: dims.thumb,
|
|
156
|
+
height: dims.thumb,
|
|
157
|
+
borderRadius: dims.thumb / 2,
|
|
158
|
+
backgroundColor: thumbBackgroundColor,
|
|
159
|
+
borderWidth: 1,
|
|
160
|
+
borderColor: thumbBorderColor,
|
|
161
|
+
...getShadowStyle("subtle"),
|
|
162
|
+
},
|
|
163
|
+
{ transform: [{ translateX: thumbTranslateX }] },
|
|
164
|
+
] })] })] }));
|
|
172
165
|
}
|
|
173
166
|
export { Slider };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { StyleProp, TextStyle } from "react-native";
|
|
3
|
+
export declare const TextClassContext: React.Context<string | undefined>;
|
|
4
|
+
export declare const TextColorContext: React.Context<string | undefined>;
|
|
5
|
+
export declare const TextStyleContext: React.Context<StyleProp<TextStyle>>;
|
|
6
|
+
export declare const TextSelectabilityContext: React.Context<boolean | undefined>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export const TextClassContext = React.createContext(undefined);
|
|
3
|
+
export const TextColorContext = React.createContext(undefined);
|
|
4
|
+
export const TextStyleContext = React.createContext(undefined);
|
|
5
|
+
export const TextSelectabilityContext = React.createContext(undefined);
|