@mrmeg/expo-ui 0.6.1 → 0.7.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/LLM_USAGE.md +9 -6
- package/README.md +11 -7
- 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 +106 -27
- 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/globalUIStore.d.ts +23 -16
- package/dist/state/themeColorScope.js +3 -3
- package/llms-full.md +5 -6
- package/llms.txt +2 -2
- package/package.json +8 -6
|
@@ -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,72 @@ 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
|
|
48
|
-
|
|
49
|
+
const opacityRef = useRef(null);
|
|
50
|
+
if (opacityRef.current === null) {
|
|
51
|
+
opacityRef.current = new Animated.Value(0);
|
|
52
|
+
}
|
|
53
|
+
const opacity = opacityRef.current;
|
|
54
|
+
const translateYRef = useRef(null);
|
|
55
|
+
if (translateYRef.current === null) {
|
|
56
|
+
translateYRef.current = new Animated.Value(0);
|
|
57
|
+
}
|
|
58
|
+
const translateY = translateYRef.current;
|
|
49
59
|
const wasVisibleRef = useRef(false);
|
|
50
60
|
const timerRef = useRef(null);
|
|
51
61
|
const hideNotification = useCallback(() => {
|
|
52
62
|
hide();
|
|
53
63
|
}, [hide]);
|
|
54
|
-
const
|
|
55
|
-
|
|
64
|
+
const clearAutoDismissTimer = useCallback(() => {
|
|
65
|
+
if (timerRef.current) {
|
|
66
|
+
clearTimeout(timerRef.current);
|
|
67
|
+
timerRef.current = null;
|
|
68
|
+
}
|
|
69
|
+
}, []);
|
|
56
70
|
const animateOut = useCallback(() => {
|
|
71
|
+
clearAutoDismissTimer();
|
|
57
72
|
if (reduceMotion) {
|
|
58
|
-
opacity.
|
|
73
|
+
opacity.setValue(0);
|
|
59
74
|
hideNotification();
|
|
60
75
|
return;
|
|
61
76
|
}
|
|
62
77
|
const slideTarget = isBottom ? 8 : -8;
|
|
63
|
-
|
|
64
|
-
|
|
78
|
+
Animated.parallel([
|
|
79
|
+
Animated.timing(opacity, {
|
|
80
|
+
toValue: 0,
|
|
81
|
+
...timingOut,
|
|
82
|
+
}),
|
|
83
|
+
Animated.timing(translateY, {
|
|
84
|
+
toValue: slideTarget,
|
|
85
|
+
...timingOut,
|
|
86
|
+
}),
|
|
87
|
+
]).start(({ finished }) => {
|
|
65
88
|
if (finished)
|
|
66
|
-
|
|
89
|
+
hideNotification();
|
|
67
90
|
});
|
|
68
|
-
}, [reduceMotion, isBottom, opacity, translateY, hideNotification]);
|
|
91
|
+
}, [clearAutoDismissTimer, reduceMotion, isBottom, opacity, translateY, hideNotification]);
|
|
92
|
+
const handleActionPress = useCallback(() => {
|
|
93
|
+
const action = alert?.action;
|
|
94
|
+
if (!action)
|
|
95
|
+
return;
|
|
96
|
+
try {
|
|
97
|
+
action.onPress();
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
animateOut();
|
|
101
|
+
}
|
|
102
|
+
}, [alert?.action, animateOut]);
|
|
103
|
+
// The auto-dismiss timer only needs the latest animateOut; wrapping it in an
|
|
104
|
+
// Effect Event keeps it out of the deps so the effect doesn't re-run (and
|
|
105
|
+
// restart the timer) every time animateOut's identity changes.
|
|
106
|
+
const onAutoDismiss = useEffectEvent(() => {
|
|
107
|
+
animateOut();
|
|
108
|
+
});
|
|
69
109
|
useEffect(() => {
|
|
70
110
|
const isNowVisible = alert?.show ?? false;
|
|
71
111
|
const wasVisible = wasVisibleRef.current;
|
|
@@ -76,20 +116,28 @@ export const Notification = () => {
|
|
|
76
116
|
}
|
|
77
117
|
const slideFrom = isBottom ? 8 : -8;
|
|
78
118
|
if (reduceMotion) {
|
|
79
|
-
opacity.
|
|
80
|
-
translateY.
|
|
119
|
+
opacity.setValue(1);
|
|
120
|
+
translateY.setValue(0);
|
|
81
121
|
}
|
|
82
122
|
else {
|
|
83
|
-
opacity.
|
|
84
|
-
translateY.
|
|
85
|
-
|
|
86
|
-
|
|
123
|
+
opacity.setValue(0);
|
|
124
|
+
translateY.setValue(slideFrom);
|
|
125
|
+
Animated.parallel([
|
|
126
|
+
Animated.timing(opacity, {
|
|
127
|
+
toValue: 1,
|
|
128
|
+
...timingIn,
|
|
129
|
+
}),
|
|
130
|
+
Animated.timing(translateY, {
|
|
131
|
+
toValue: 0,
|
|
132
|
+
...timingIn,
|
|
133
|
+
}),
|
|
134
|
+
]).start();
|
|
87
135
|
}
|
|
88
136
|
}
|
|
89
137
|
wasVisibleRef.current = isNowVisible;
|
|
90
138
|
if (isNowVisible && !wasVisible && alert?.duration) {
|
|
91
139
|
timerRef.current = setTimeout(() => {
|
|
92
|
-
|
|
140
|
+
onAutoDismiss();
|
|
93
141
|
}, alert.duration);
|
|
94
142
|
return () => {
|
|
95
143
|
if (timerRef.current) {
|
|
@@ -98,11 +146,12 @@ export const Notification = () => {
|
|
|
98
146
|
}
|
|
99
147
|
};
|
|
100
148
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
149
|
+
// onAutoDismiss is an Effect Event — intentionally omitted from deps.
|
|
150
|
+
}, [alert, reduceMotion, isBottom, opacity, translateY]);
|
|
151
|
+
const animatedContainerStyle = {
|
|
152
|
+
opacity,
|
|
153
|
+
transform: [{ translateY }],
|
|
154
|
+
};
|
|
106
155
|
const topPosition = insets?.top ? insets.top : 20;
|
|
107
156
|
const bottomPosition = insets?.bottom ? insets.bottom : 20;
|
|
108
157
|
const getIconProps = () => {
|
|
@@ -157,6 +206,7 @@ export const Notification = () => {
|
|
|
157
206
|
const message = alert?.messages?.find((item) => item.trim().length > 0);
|
|
158
207
|
const title = getTitle(message);
|
|
159
208
|
const hasMessage = !!message;
|
|
209
|
+
const action = alert?.action;
|
|
160
210
|
if (!alert?.show) {
|
|
161
211
|
return null;
|
|
162
212
|
}
|
|
@@ -172,14 +222,23 @@ export const Notification = () => {
|
|
|
172
222
|
styles.alert,
|
|
173
223
|
isBottom && styles.alertBottom,
|
|
174
224
|
getShadowStyle("base"),
|
|
175
|
-
], children: [_jsx(View, { style: [styles.iconBadge, { backgroundColor: iconBgColor }], children: alert?.loading ? (_jsx(ActivityIndicator, { size: "small", color: iconColor })) : (_jsx(Icon, { name: icon, size: 18, color: iconColor })) }), _jsxs(View, { style: styles.alertContent, children: [!!title && (_jsx(StyledText, { selectable: false, style: [styles.alertTitle, { color: theme.colors.foreground }], numberOfLines: 1, children: title })), hasMessage && (_jsx(StyledText, { selectable: false, style: [styles.alertDescription, { color: theme.colors.mutedForeground }], numberOfLines: 2, children: message }))] }), _jsx(Pressable, { style:
|
|
225
|
+
], children: [_jsx(View, { style: [styles.iconBadge, { backgroundColor: iconBgColor }], children: alert?.loading ? (_jsx(ActivityIndicator, { size: "small", color: iconColor })) : (_jsx(Icon, { name: icon, size: 18, color: iconColor })) }), _jsxs(View, { style: styles.alertContent, children: [!!title && (_jsx(StyledText, { selectable: false, style: [styles.alertTitle, { color: theme.colors.foreground }], numberOfLines: 1, children: title })), hasMessage && (_jsx(StyledText, { selectable: false, style: [styles.alertDescription, { color: theme.colors.mutedForeground }], numberOfLines: 2, children: message }))] }), action && (_jsx(Pressable, { style: ({ pressed }) => [
|
|
226
|
+
styles.actionButton,
|
|
227
|
+
{
|
|
228
|
+
borderColor: theme.colors.primary + "30",
|
|
229
|
+
backgroundColor: theme.colors.primary + "10",
|
|
230
|
+
},
|
|
231
|
+
pressed && styles.actionButtonPressed,
|
|
232
|
+
], hitSlop: spacing.xs, onPress: handleActionPress, accessibilityLabel: action.label, accessibilityRole: "button", children: _jsx(StyledText, { selectable: false, style: [styles.actionLabel, { color: theme.colors.primary }], numberOfLines: 1, children: action.label }) })), _jsx(Pressable, { style: styles.closeButton, hitSlop: spacing.sm, onPress: animateOut, accessibilityLabel: "Dismiss notification", accessibilityRole: "button", children: _jsx(Icon, { name: "x", size: 16, color: theme.colors.mutedForeground }) })] }) }));
|
|
176
233
|
};
|
|
177
234
|
const createStyles = (theme) => StyleSheet.create({
|
|
178
235
|
container: {
|
|
179
236
|
position: "absolute",
|
|
180
237
|
left: spacing.md,
|
|
181
238
|
right: spacing.md,
|
|
182
|
-
|
|
239
|
+
// Toast sits above the overlay layer (dialogs/drawers/dropdowns top out
|
|
240
|
+
// around 52); no need to escalate into the hundreds.
|
|
241
|
+
zIndex: 60,
|
|
183
242
|
alignItems: "center",
|
|
184
243
|
},
|
|
185
244
|
alert: {
|
|
@@ -223,6 +282,26 @@ const createStyles = (theme) => StyleSheet.create({
|
|
|
223
282
|
fontSize: 13,
|
|
224
283
|
lineHeight: 18,
|
|
225
284
|
},
|
|
285
|
+
actionButton: {
|
|
286
|
+
minHeight: 28,
|
|
287
|
+
maxWidth: 140,
|
|
288
|
+
paddingHorizontal: spacing.sm,
|
|
289
|
+
borderRadius: spacing.radiusSm,
|
|
290
|
+
borderWidth: 1,
|
|
291
|
+
justifyContent: "center",
|
|
292
|
+
alignItems: "center",
|
|
293
|
+
flexShrink: 0,
|
|
294
|
+
...(Platform.OS === "web" && { cursor: "pointer" }),
|
|
295
|
+
},
|
|
296
|
+
actionButtonPressed: {
|
|
297
|
+
opacity: 0.75,
|
|
298
|
+
},
|
|
299
|
+
actionLabel: {
|
|
300
|
+
fontFamily: fontFamilies.sansSerif.regular,
|
|
301
|
+
fontWeight: "600",
|
|
302
|
+
fontSize: 13,
|
|
303
|
+
lineHeight: 18,
|
|
304
|
+
},
|
|
226
305
|
closeButton: {
|
|
227
306
|
position: "absolute",
|
|
228
307
|
top: spacing.sm,
|
|
@@ -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
|
}
|