@mrmeg/expo-ui 0.6.0 → 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 +16 -12
- package/dist/components/DropdownMenu.js +32 -30
- 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
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo, useCallback,
|
|
3
|
-
import { StyleSheet, View, ActivityIndicator, Pressable, Platform } from "react-native";
|
|
4
|
-
import Animated, { useSharedValue, useAnimatedStyle, withTiming, runOnJS, useReducedMotion, Easing, } from "react-native-reanimated";
|
|
2
|
+
import { useMemo, useCallback, use, useEffect, useEffectEvent, useRef } from "react";
|
|
3
|
+
import { Animated, Easing, StyleSheet, View, ActivityIndicator, Pressable, Platform } from "react-native";
|
|
5
4
|
import { SafeAreaInsetsContext } from "react-native-safe-area-context";
|
|
6
5
|
import { fontFamilies } from "../constants/fonts.js";
|
|
7
6
|
import { Icon } from "./Icon.js";
|
|
8
7
|
import { useTheme } from "../hooks/useTheme.js";
|
|
8
|
+
import { useReducedMotion } from "../hooks/useReduceMotion.js";
|
|
9
9
|
import { spacing } from "../constants/spacing.js";
|
|
10
10
|
import { StyledText } from "./StyledText.js";
|
|
11
11
|
import { translateText } from "../lib/i18n.js";
|
|
12
12
|
import { globalUIStore } from "../state/globalUIStore.js";
|
|
13
|
+
const timingIn = { duration: 150, easing: Easing.out(Easing.quad), useNativeDriver: true };
|
|
14
|
+
const timingOut = { duration: 100, easing: Easing.in(Easing.quad), useNativeDriver: true };
|
|
13
15
|
/**
|
|
14
16
|
* Notification
|
|
15
17
|
*
|
|
@@ -38,34 +40,46 @@ import { globalUIStore } from "../state/globalUIStore.js";
|
|
|
38
40
|
export const Notification = () => {
|
|
39
41
|
const { theme, getShadowStyle } = useTheme();
|
|
40
42
|
const reduceMotion = useReducedMotion();
|
|
41
|
-
const insets =
|
|
43
|
+
const insets = use(SafeAreaInsetsContext);
|
|
42
44
|
const { alert, hide } = globalUIStore();
|
|
43
45
|
const styles = useMemo(() => createStyles(theme), [theme]);
|
|
44
46
|
const position = alert?.position ?? "top";
|
|
45
47
|
const isBottom = position === "bottom";
|
|
46
48
|
// Just opacity + translateY — no scale (scale = bouncy feel)
|
|
47
|
-
const opacity =
|
|
48
|
-
const translateY =
|
|
49
|
+
const opacity = useRef(new Animated.Value(0)).current;
|
|
50
|
+
const translateY = useRef(new Animated.Value(0)).current;
|
|
49
51
|
const wasVisibleRef = useRef(false);
|
|
50
52
|
const timerRef = useRef(null);
|
|
51
53
|
const hideNotification = useCallback(() => {
|
|
52
54
|
hide();
|
|
53
55
|
}, [hide]);
|
|
54
|
-
const timingIn = { duration: 150, easing: Easing.out(Easing.quad) };
|
|
55
|
-
const timingOut = { duration: 100, easing: Easing.in(Easing.quad) };
|
|
56
56
|
const animateOut = useCallback(() => {
|
|
57
57
|
if (reduceMotion) {
|
|
58
|
-
opacity.
|
|
58
|
+
opacity.setValue(0);
|
|
59
59
|
hideNotification();
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
62
|
const slideTarget = isBottom ? 8 : -8;
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
Animated.parallel([
|
|
64
|
+
Animated.timing(opacity, {
|
|
65
|
+
toValue: 0,
|
|
66
|
+
...timingOut,
|
|
67
|
+
}),
|
|
68
|
+
Animated.timing(translateY, {
|
|
69
|
+
toValue: slideTarget,
|
|
70
|
+
...timingOut,
|
|
71
|
+
}),
|
|
72
|
+
]).start(({ finished }) => {
|
|
65
73
|
if (finished)
|
|
66
|
-
|
|
74
|
+
hideNotification();
|
|
67
75
|
});
|
|
68
76
|
}, [reduceMotion, isBottom, opacity, translateY, hideNotification]);
|
|
77
|
+
// The auto-dismiss timer only needs the latest animateOut; wrapping it in an
|
|
78
|
+
// Effect Event keeps it out of the deps so the effect doesn't re-run (and
|
|
79
|
+
// restart the timer) every time animateOut's identity changes.
|
|
80
|
+
const onAutoDismiss = useEffectEvent(() => {
|
|
81
|
+
animateOut();
|
|
82
|
+
});
|
|
69
83
|
useEffect(() => {
|
|
70
84
|
const isNowVisible = alert?.show ?? false;
|
|
71
85
|
const wasVisible = wasVisibleRef.current;
|
|
@@ -76,20 +90,28 @@ export const Notification = () => {
|
|
|
76
90
|
}
|
|
77
91
|
const slideFrom = isBottom ? 8 : -8;
|
|
78
92
|
if (reduceMotion) {
|
|
79
|
-
opacity.
|
|
80
|
-
translateY.
|
|
93
|
+
opacity.setValue(1);
|
|
94
|
+
translateY.setValue(0);
|
|
81
95
|
}
|
|
82
96
|
else {
|
|
83
|
-
opacity.
|
|
84
|
-
translateY.
|
|
85
|
-
|
|
86
|
-
|
|
97
|
+
opacity.setValue(0);
|
|
98
|
+
translateY.setValue(slideFrom);
|
|
99
|
+
Animated.parallel([
|
|
100
|
+
Animated.timing(opacity, {
|
|
101
|
+
toValue: 1,
|
|
102
|
+
...timingIn,
|
|
103
|
+
}),
|
|
104
|
+
Animated.timing(translateY, {
|
|
105
|
+
toValue: 0,
|
|
106
|
+
...timingIn,
|
|
107
|
+
}),
|
|
108
|
+
]).start();
|
|
87
109
|
}
|
|
88
110
|
}
|
|
89
111
|
wasVisibleRef.current = isNowVisible;
|
|
90
112
|
if (isNowVisible && !wasVisible && alert?.duration) {
|
|
91
113
|
timerRef.current = setTimeout(() => {
|
|
92
|
-
|
|
114
|
+
onAutoDismiss();
|
|
93
115
|
}, alert.duration);
|
|
94
116
|
return () => {
|
|
95
117
|
if (timerRef.current) {
|
|
@@ -98,11 +120,12 @@ export const Notification = () => {
|
|
|
98
120
|
}
|
|
99
121
|
};
|
|
100
122
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
123
|
+
// onAutoDismiss is an Effect Event — intentionally omitted from deps.
|
|
124
|
+
}, [alert, reduceMotion, isBottom, opacity, translateY]);
|
|
125
|
+
const animatedContainerStyle = {
|
|
126
|
+
opacity,
|
|
127
|
+
transform: [{ translateY }],
|
|
128
|
+
};
|
|
106
129
|
const topPosition = insets?.top ? insets.top : 20;
|
|
107
130
|
const bottomPosition = insets?.bottom ? insets.bottom : 20;
|
|
108
131
|
const getIconProps = () => {
|
|
@@ -179,7 +202,9 @@ const createStyles = (theme) => StyleSheet.create({
|
|
|
179
202
|
position: "absolute",
|
|
180
203
|
left: spacing.md,
|
|
181
204
|
right: spacing.md,
|
|
182
|
-
|
|
205
|
+
// Toast sits above the overlay layer (dialogs/drawers/dropdowns top out
|
|
206
|
+
// around 52); no need to escalate into the hundreds.
|
|
207
|
+
zIndex: 60,
|
|
183
208
|
alignItems: "center",
|
|
184
209
|
},
|
|
185
210
|
alert: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { AnimatedView } from "./AnimatedView.js";
|
|
3
|
-
import { TextClassContext, TextColorContext } from "./StyledText.
|
|
3
|
+
import { TextClassContext, TextColorContext } from "./StyledText.context";
|
|
4
4
|
import { useTheme } from "../hooks/useTheme.js";
|
|
5
5
|
import { spacing } from "../constants/spacing.js";
|
|
6
6
|
import * as PopoverPrimitive from "@rn-primitives/popover";
|
|
@@ -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
|
}
|