@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
package/dist/components/Card.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext,
|
|
3
|
-
import { View, Pressable, StyleSheet, Platform } from "react-native";
|
|
4
|
-
import Animated from "react-native-reanimated";
|
|
2
|
+
import { createContext, use } from "react";
|
|
3
|
+
import { View, Pressable, StyleSheet, Platform, Animated } from "react-native";
|
|
5
4
|
import { StyledText } from "./StyledText.js";
|
|
6
5
|
import { useTheme } from "../hooks/useTheme.js";
|
|
7
6
|
import { useScalePress } from "../hooks/useScalePress.js";
|
|
@@ -30,13 +29,12 @@ import { spacing } from "../constants/spacing.js";
|
|
|
30
29
|
*/
|
|
31
30
|
const CardContext = createContext(null);
|
|
32
31
|
function useCardContext() {
|
|
33
|
-
const ctx =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
return ctx;
|
|
32
|
+
const ctx = use(CardContext);
|
|
33
|
+
// useTheme must run unconditionally (Rules of Hooks); createCardStyles is a
|
|
34
|
+
// plain function, so the `??` keeps the fallback styles lazy.
|
|
35
|
+
const { theme } = useTheme();
|
|
36
|
+
// Fallback for standalone usage without a Card parent.
|
|
37
|
+
return ctx ?? { theme, styles: createCardStyles(theme) };
|
|
40
38
|
}
|
|
41
39
|
function Card({ children, style: styleOverride, variant = "default", onPress, disabled }) {
|
|
42
40
|
const { theme, getShadowStyle } = useTheme();
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect } from "react";
|
|
3
|
-
import { View, StyleSheet, Pressable, Platform } from "react-native";
|
|
4
|
-
import Animated, { useSharedValue, useAnimatedStyle, withTiming, useReducedMotion, } from "react-native-reanimated";
|
|
2
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
3
|
+
import { View, StyleSheet, Pressable, Platform, Animated } from "react-native";
|
|
5
4
|
import { Icon } from "./Icon.js";
|
|
6
5
|
import { StyledText } from "./StyledText.js";
|
|
7
6
|
import { useTheme } from "../hooks/useTheme.js";
|
|
8
7
|
import { spacing } from "../constants/spacing.js";
|
|
9
8
|
import { hapticLight } from "../lib/haptics.js";
|
|
9
|
+
import { useReducedMotion } from "../hooks/useReduceMotion.js";
|
|
10
10
|
import * as CheckboxPrimitive from "@rn-primitives/checkbox";
|
|
11
11
|
const DEFAULT_HIT_SLOP = 8;
|
|
12
12
|
const SIZE_CONFIGS = {
|
|
@@ -19,30 +19,24 @@ function Checkbox({ size = "md", label, indeterminate = false, error = false, st
|
|
|
19
19
|
const reduceMotion = useReducedMotion();
|
|
20
20
|
const sizeConfig = SIZE_CONFIGS[size];
|
|
21
21
|
// Simple fast opacity for the checkmark icon
|
|
22
|
-
const checkOpacity =
|
|
22
|
+
const checkOpacity = useRef(new Animated.Value(checked || indeterminate ? 1 : 0)).current;
|
|
23
23
|
const isVisuallyChecked = !!checked || indeterminate;
|
|
24
|
+
const animateCheckOpacity = useCallback((nextVisible) => {
|
|
25
|
+
Animated.timing(checkOpacity, {
|
|
26
|
+
toValue: nextVisible ? 1 : 0,
|
|
27
|
+
duration: reduceMotion ? 0 : 60,
|
|
28
|
+
useNativeDriver: true,
|
|
29
|
+
}).start();
|
|
30
|
+
}, [checkOpacity, reduceMotion]);
|
|
24
31
|
useEffect(() => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
checkOpacity.value = withTiming(isVisuallyChecked ? 1 : 0, { duration: 60 });
|
|
30
|
-
}
|
|
31
|
-
}, [checkOpacity, isVisuallyChecked, reduceMotion]);
|
|
32
|
+
animateCheckOpacity(isVisuallyChecked);
|
|
33
|
+
}, [animateCheckOpacity, isVisuallyChecked]);
|
|
32
34
|
const wrappedOnCheckedChange = (next) => {
|
|
33
35
|
if (next)
|
|
34
36
|
hapticLight();
|
|
35
|
-
|
|
36
|
-
checkOpacity.value = withTiming(next ? 1 : 0, { duration: 0 });
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
checkOpacity.value = withTiming(next ? 1 : 0, { duration: 60 });
|
|
40
|
-
}
|
|
37
|
+
animateCheckOpacity(next);
|
|
41
38
|
onCheckedChange?.(next);
|
|
42
39
|
};
|
|
43
|
-
const checkAnimatedStyle = useAnimatedStyle(() => ({
|
|
44
|
-
opacity: checkOpacity.value,
|
|
45
|
-
}));
|
|
46
40
|
// Dynamic border color with sufficient contrast against background
|
|
47
41
|
const borderColor = error
|
|
48
42
|
? theme.colors.destructive
|
|
@@ -52,14 +46,11 @@ function Checkbox({ size = "md", label, indeterminate = false, error = false, st
|
|
|
52
46
|
// Flatten style override for web compatibility
|
|
53
47
|
const flattenedStyle = styleOverride ? StyleSheet.flatten(styleOverride) : undefined;
|
|
54
48
|
const checkboxElement = (_jsx(CheckboxPrimitive.Root, { ...props, checked: checked, onCheckedChange: wrappedOnCheckedChange, disabled: disabled, style: {
|
|
49
|
+
...styles.box,
|
|
55
50
|
borderColor,
|
|
56
51
|
backgroundColor: isVisuallyChecked ? theme.colors.primary : theme.colors.background,
|
|
57
|
-
borderRadius: spacing.radiusSm,
|
|
58
|
-
borderWidth: 1,
|
|
59
52
|
width: sizeConfig.size,
|
|
60
53
|
height: sizeConfig.size,
|
|
61
|
-
justifyContent: "center",
|
|
62
|
-
alignItems: "center",
|
|
63
54
|
opacity: disabled ? 0.5 : 1,
|
|
64
55
|
...(Platform.OS === "web" && { cursor: disabled ? "not-allowed" : "pointer" }),
|
|
65
56
|
...(flattenedStyle || {}),
|
|
@@ -69,7 +60,7 @@ function Checkbox({ size = "md", label, indeterminate = false, error = false, st
|
|
|
69
60
|
}, accessibilityLabel: label, children: _jsx(CheckboxPrimitive.Indicator, { style: {
|
|
70
61
|
justifyContent: "center",
|
|
71
62
|
alignItems: "center",
|
|
72
|
-
}, children: _jsx(Animated.View, { style:
|
|
63
|
+
}, children: _jsx(Animated.View, { style: { opacity: checkOpacity }, children: indeterminate ? (_jsx(Icon, { name: "minus", size: sizeConfig.iconSize, color: theme.colors.primaryForeground })) : (_jsx(Icon, { name: "check", size: sizeConfig.iconSize, color: theme.colors.primaryForeground })) }) }) }));
|
|
73
64
|
// If no label, return just the checkbox
|
|
74
65
|
if (!label) {
|
|
75
66
|
return checkboxElement;
|
|
@@ -86,6 +77,12 @@ function Checkbox({ size = "md", label, indeterminate = false, error = false, st
|
|
|
86
77
|
], children: [label, required && (_jsx(StyledText, { selectable: false, style: [styles.required, { color: theme.colors.destructive }], children: " *" }))] }) })] }));
|
|
87
78
|
}
|
|
88
79
|
const styles = StyleSheet.create({
|
|
80
|
+
box: {
|
|
81
|
+
borderRadius: spacing.radiusSm,
|
|
82
|
+
borderWidth: 1,
|
|
83
|
+
justifyContent: "center",
|
|
84
|
+
alignItems: "center",
|
|
85
|
+
},
|
|
89
86
|
container: {
|
|
90
87
|
flexDirection: "row",
|
|
91
88
|
alignItems: "center",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import { TextClassContext, TextSelectabilityContext } from "./StyledText.js";
|
|
2
|
+
import { Platform, StyleSheet, View } from "react-native";
|
|
3
|
+
import { TextClassContext, TextSelectabilityContext } from "./StyledText.context";
|
|
5
4
|
import { spacing } from "../constants/spacing.js";
|
|
6
5
|
import { useTheme } from "../hooks/useTheme.js";
|
|
7
6
|
import * as CollapsiblePrimitive from "@rn-primitives/collapsible";
|
|
@@ -26,11 +25,8 @@ function CollapsibleTrigger({ style: styleOverride, ...props }) {
|
|
|
26
25
|
} }) }) }));
|
|
27
26
|
}
|
|
28
27
|
function CollapsibleContent({ forceMount, style: styleOverride, children, ...props }) {
|
|
29
|
-
|
|
30
|
-
const fadeAnim = React.useRef(new Animated.Value(1)).current;
|
|
31
|
-
return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(CollapsiblePrimitive.Content, { ...props, forceMount: forceMount, children: _jsx(Animated.View, { style: {
|
|
28
|
+
return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(CollapsiblePrimitive.Content, { ...props, forceMount: forceMount, children: _jsx(View, { style: {
|
|
32
29
|
overflow: "hidden",
|
|
33
|
-
opacity: fadeAnim,
|
|
34
30
|
...(styleOverride && typeof styleOverride !== "function"
|
|
35
31
|
? StyleSheet.flatten(styleOverride)
|
|
36
32
|
: {}),
|
|
@@ -5,7 +5,7 @@ import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
|
|
|
5
5
|
import * as DialogPrimitive from "@rn-primitives/dialog";
|
|
6
6
|
import * as AlertDialogPrimitive from "@rn-primitives/alert-dialog";
|
|
7
7
|
import { AnimatedView } from "./AnimatedView.js";
|
|
8
|
-
import { TextClassContext, TextColorContext } from "./StyledText.
|
|
8
|
+
import { TextClassContext, TextColorContext } from "./StyledText.context";
|
|
9
9
|
import { StyledText } from "./StyledText.js";
|
|
10
10
|
import { useTheme } from "../hooks/useTheme.js";
|
|
11
11
|
import { spacing } from "../constants/spacing.js";
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Pressable, Keyboard, Platform, KeyboardAvoidingView, ScrollView } from "react-native";
|
|
3
|
+
const handleDismissKeyboard = () => Platform.OS !== "web" && Keyboard.dismiss();
|
|
3
4
|
/**
|
|
4
5
|
* @returns Wrapper for a view that dismisses the keyboard when tapped outside of a text input
|
|
5
6
|
*/
|
|
6
7
|
export const DismissKeyboard = ({ children, style, avoidKeyboard = true, scrollable = true }) => {
|
|
7
|
-
const handlePress = () => Platform.OS !== "web" && Keyboard.dismiss();
|
|
8
8
|
const content = scrollable ? (_jsx(ScrollView, { style: { flex: 1 }, contentContainerStyle: { flexGrow: 1, justifyContent: "center" }, keyboardShouldPersistTaps: "handled", showsVerticalScrollIndicator: false, children: children })) : (children);
|
|
9
9
|
if (!avoidKeyboard) {
|
|
10
|
-
return (_jsx(Pressable, { onPress:
|
|
10
|
+
return (_jsx(Pressable, { onPress: handleDismissKeyboard, accessible: false, style: { flex: 1 }, children: content }));
|
|
11
11
|
}
|
|
12
|
-
return (_jsx(KeyboardAvoidingView, { style: [{ flex: 1, width: "100%" }, style], behavior: Platform.OS === "ios" ? "padding" : "height", keyboardVerticalOffset: Platform.OS === "ios" ? 0 : 0, children: _jsx(Pressable, { onPress:
|
|
12
|
+
return (_jsx(KeyboardAvoidingView, { style: [{ flex: 1, width: "100%" }, style], behavior: Platform.OS === "ios" ? "padding" : "height", keyboardVerticalOffset: Platform.OS === "ios" ? 0 : 0, children: _jsx(Pressable, { onPress: handleDismissKeyboard, accessible: false, style: { flex: 1 }, children: content }) }));
|
|
13
13
|
};
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { createContext,
|
|
3
|
-
import { View, Pressable, Animated, StyleSheet, Platform,
|
|
2
|
+
import React, { createContext, use, useState, useReducer, useRef } from "react";
|
|
3
|
+
import { View, Pressable, Animated, StyleSheet, Platform, PanResponder, } from "react-native";
|
|
4
4
|
import { Portal } from "@rn-primitives/portal";
|
|
5
5
|
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
|
|
6
6
|
import { Pressable as SlotPressable } from "@rn-primitives/slot";
|
|
7
7
|
import { useTheme } from "../hooks/useTheme.js";
|
|
8
|
+
import { useDimensions } from "../hooks/useDimensions.js";
|
|
8
9
|
import { shouldUseNativeDriver } from "../lib/animations.js";
|
|
9
10
|
import { spacing } from "../constants/spacing.js";
|
|
10
|
-
import { TextColorContext, TextClassContext } from "./StyledText.
|
|
11
|
+
import { TextColorContext, TextClassContext } from "./StyledText.context";
|
|
11
12
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
12
13
|
/**
|
|
13
14
|
* Drawer Component with Sub-components
|
|
@@ -50,7 +51,7 @@ const FullWindowOverlay = Platform.OS === "ios" ? RNFullWindowOverlay : React.Fr
|
|
|
50
51
|
// ============================================================================
|
|
51
52
|
const DrawerContext = createContext(null);
|
|
52
53
|
function useDrawerContext() {
|
|
53
|
-
const context =
|
|
54
|
+
const context = use(DrawerContext);
|
|
54
55
|
if (!context) {
|
|
55
56
|
throw new Error("Drawer components must be used within a Drawer");
|
|
56
57
|
}
|
|
@@ -59,13 +60,12 @@ function useDrawerContext() {
|
|
|
59
60
|
// ============================================================================
|
|
60
61
|
// Utility Functions
|
|
61
62
|
// ============================================================================
|
|
62
|
-
function parseWidth(width) {
|
|
63
|
+
function parseWidth(width, screenWidth) {
|
|
63
64
|
if (typeof width === "number") {
|
|
64
65
|
return width;
|
|
65
66
|
}
|
|
66
67
|
// Parse percentage string
|
|
67
68
|
const percentage = parseFloat(width) / 100;
|
|
68
|
-
const screenWidth = Dimensions.get("window").width;
|
|
69
69
|
return screenWidth * percentage;
|
|
70
70
|
}
|
|
71
71
|
function drawerReducer(state, action) {
|
|
@@ -103,7 +103,9 @@ function DrawerRoot({ open: controlledOpen, onOpenChange: controlledOnOpenChange
|
|
|
103
103
|
dispatch({ type: newOpen ? "OPEN" : "CLOSE" });
|
|
104
104
|
}
|
|
105
105
|
};
|
|
106
|
-
|
|
106
|
+
// useDimensions reacts to rotation / split-screen, unlike Dimensions.get.
|
|
107
|
+
const { width: screenWidth } = useDimensions();
|
|
108
|
+
const parsedWidth = parseWidth(width, screenWidth);
|
|
107
109
|
const contextValue = {
|
|
108
110
|
open,
|
|
109
111
|
onOpenChange,
|
|
@@ -148,10 +150,19 @@ function DrawerContent({ swipeEnabled = true, swipeThreshold = 0.3, velocityThre
|
|
|
148
150
|
const { open, onOpenChange, side, width, closeOnBackdropPress } = drawerContext;
|
|
149
151
|
const { theme, getShadowStyle } = useTheme();
|
|
150
152
|
const insets = useSafeAreaInsets();
|
|
151
|
-
// Animation values - initialize
|
|
153
|
+
// Animation values - initialize lazily so the Animated.Value is allocated
|
|
154
|
+
// once on first render instead of being rebuilt and discarded every render.
|
|
152
155
|
const closedPosition = side === "left" ? -width : width;
|
|
153
|
-
const
|
|
154
|
-
|
|
156
|
+
const translateXRef = useRef(null);
|
|
157
|
+
if (translateXRef.current === null) {
|
|
158
|
+
translateXRef.current = new Animated.Value(open ? 0 : closedPosition);
|
|
159
|
+
}
|
|
160
|
+
const translateX = translateXRef.current;
|
|
161
|
+
const backdropOpacityRef = useRef(null);
|
|
162
|
+
if (backdropOpacityRef.current === null) {
|
|
163
|
+
backdropOpacityRef.current = new Animated.Value(open ? 1 : 0);
|
|
164
|
+
}
|
|
165
|
+
const backdropOpacity = backdropOpacityRef.current;
|
|
155
166
|
// Track if drawer is actually visible (for unmounting after close animation)
|
|
156
167
|
const [isVisible, setIsVisible] = useState(open);
|
|
157
168
|
// Track what we last animated to - persists across renders
|
|
@@ -9,15 +9,6 @@ declare const DropdownMenu: {
|
|
|
9
9
|
} & React.RefAttributes<View>): React.JSX.Element;
|
|
10
10
|
displayName: string;
|
|
11
11
|
};
|
|
12
|
-
declare const DropdownMenuTrigger: {
|
|
13
|
-
({ asChild, onPress: onPressProp, disabled, ref, ...props }: Omit<import("react-native").PressableProps & React.RefAttributes<View>, "ref"> & {
|
|
14
|
-
asChild?: boolean;
|
|
15
|
-
} & {
|
|
16
|
-
onKeyDown?: (ev: React.KeyboardEvent) => void;
|
|
17
|
-
onKeyUp?: (ev: React.KeyboardEvent) => void;
|
|
18
|
-
} & React.RefAttributes<import("@rn-primitives/dropdown-menu").TriggerRef>): React.JSX.Element;
|
|
19
|
-
displayName: string;
|
|
20
|
-
};
|
|
21
12
|
declare const DropdownMenuGroup: {
|
|
22
13
|
({ asChild, ref, ...props }: import("react-native").ViewProps & {
|
|
23
14
|
asChild?: boolean;
|
|
@@ -44,6 +35,18 @@ declare const DropdownMenuRadioGroup: {
|
|
|
44
35
|
} & React.RefAttributes<View>): React.JSX.Element;
|
|
45
36
|
displayName: string;
|
|
46
37
|
};
|
|
38
|
+
/**
|
|
39
|
+
* DropdownMenuTrigger Component
|
|
40
|
+
* Wraps the primitive Trigger to default `accessibilityRole="button"`.
|
|
41
|
+
*
|
|
42
|
+
* The underlying @rn-primitives Trigger renders a RN-Web Pressable (or a Slot
|
|
43
|
+
* when `asChild`) without a role, so on web it becomes `role="generic"` —
|
|
44
|
+
* breaking screen-reader semantics and `getByRole("button")` queries. When
|
|
45
|
+
* `asChild` is used, the child's own role still wins (the Slot merges child
|
|
46
|
+
* props over slot props), so this only fills the gap when none is set.
|
|
47
|
+
*/
|
|
48
|
+
type DropdownMenuTriggerProps = DropdownMenuPrimitive.TriggerProps;
|
|
49
|
+
declare function DropdownMenuTrigger({ ...props }: DropdownMenuTriggerProps): import("react/jsx-runtime").JSX.Element;
|
|
47
50
|
/**
|
|
48
51
|
* DropdownMenuSubTrigger Component
|
|
49
52
|
* Trigger for sub-menus with automatic chevron icon
|
|
@@ -112,9 +115,10 @@ declare function DropdownMenuSeparator({ style: styleOverride, ...props }: Dropd
|
|
|
112
115
|
* Text component for displaying keyboard shortcuts
|
|
113
116
|
*/
|
|
114
117
|
interface DropdownMenuShortcutProps {
|
|
115
|
-
children
|
|
118
|
+
children?: React.ReactNode;
|
|
119
|
+
text?: string;
|
|
116
120
|
style?: TextStyle;
|
|
117
121
|
}
|
|
118
|
-
declare function DropdownMenuShortcut({ style: styleOverride, ...props }: DropdownMenuShortcutProps): import("react/jsx-runtime").JSX.Element;
|
|
122
|
+
declare function DropdownMenuShortcut({ children, text, style: styleOverride, ...props }: DropdownMenuShortcutProps): import("react/jsx-runtime").JSX.Element;
|
|
119
123
|
export { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, };
|
|
120
|
-
export type { DropdownMenuSubTriggerProps, DropdownMenuSubContentProps, DropdownMenuContentProps, DropdownMenuItemProps, DropdownMenuCheckboxItemProps, DropdownMenuRadioItemProps, DropdownMenuLabelProps, DropdownMenuSeparatorProps, DropdownMenuShortcutProps, };
|
|
124
|
+
export type { DropdownMenuTriggerProps, DropdownMenuSubTriggerProps, DropdownMenuSubContentProps, DropdownMenuContentProps, DropdownMenuItemProps, DropdownMenuCheckboxItemProps, DropdownMenuRadioItemProps, DropdownMenuLabelProps, DropdownMenuSeparatorProps, DropdownMenuShortcutProps, };
|
|
@@ -3,7 +3,7 @@ import * as React from "react";
|
|
|
3
3
|
import { Platform, StyleSheet, Text, View } from "react-native";
|
|
4
4
|
import { Icon } from "./Icon.js";
|
|
5
5
|
import { AnimatedView } from "./AnimatedView.js";
|
|
6
|
-
import { TextClassContext, TextSelectabilityContext } from "./StyledText.
|
|
6
|
+
import { TextClassContext, TextSelectabilityContext } from "./StyledText.context";
|
|
7
7
|
import { useTheme } from "../hooks/useTheme.js";
|
|
8
8
|
import { spacing } from "../constants/spacing.js";
|
|
9
9
|
import * as DropdownMenuPrimitive from "@rn-primitives/dropdown-menu";
|
|
@@ -11,13 +11,15 @@ import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
|
|
|
11
11
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
12
12
|
// Re-export primitives that don't need styling
|
|
13
13
|
const DropdownMenu = DropdownMenuPrimitive.Root;
|
|
14
|
-
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
|
15
14
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
|
16
15
|
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
|
17
16
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
|
18
17
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
|
19
18
|
// Platform-specific overlay
|
|
20
19
|
const FullWindowOverlay = Platform.OS === "ios" ? RNFullWindowOverlay : React.Fragment;
|
|
20
|
+
function DropdownMenuTrigger({ ...props }) {
|
|
21
|
+
return _jsx(DropdownMenuPrimitive.Trigger, { accessibilityRole: "button", ...props });
|
|
22
|
+
}
|
|
21
23
|
function DropdownMenuSubTrigger({ inset = false, children, style: styleOverride, ...props }) {
|
|
22
24
|
const { theme } = useTheme();
|
|
23
25
|
const { open } = DropdownMenuPrimitive.useSubContext();
|
|
@@ -89,14 +91,7 @@ function DropdownMenuContent({ side, align = "start", sideOffset = 4, portalHost
|
|
|
89
91
|
function DropdownMenuItem({ inset = false, variant = "default", style: styleOverride, ...props }) {
|
|
90
92
|
const { theme } = useTheme();
|
|
91
93
|
return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(DropdownMenuPrimitive.Item, { ...props, style: {
|
|
92
|
-
|
|
93
|
-
flexDirection: "row",
|
|
94
|
-
alignItems: "center",
|
|
95
|
-
gap: spacing.sm,
|
|
96
|
-
borderRadius: spacing.radiusSm,
|
|
97
|
-
paddingHorizontal: spacing.sm,
|
|
98
|
-
paddingVertical: Platform.select({ web: spacing.xs, default: spacing.sm }),
|
|
99
|
-
backgroundColor: "transparent",
|
|
94
|
+
...styles.item,
|
|
100
95
|
...(Platform.OS === "web" && {
|
|
101
96
|
cursor: props.disabled ? "not-allowed" : "pointer",
|
|
102
97
|
outlineStyle: "none",
|
|
@@ -112,15 +107,7 @@ function DropdownMenuItem({ inset = false, variant = "default", style: styleOver
|
|
|
112
107
|
function DropdownMenuCheckboxItem({ children, style: styleOverride, ...props }) {
|
|
113
108
|
const { theme } = useTheme();
|
|
114
109
|
return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsxs(DropdownMenuPrimitive.CheckboxItem, { ...props, style: {
|
|
115
|
-
|
|
116
|
-
flexDirection: "row",
|
|
117
|
-
alignItems: "center",
|
|
118
|
-
gap: spacing.sm,
|
|
119
|
-
borderRadius: spacing.radiusSm,
|
|
120
|
-
paddingVertical: Platform.select({ web: spacing.xs, default: spacing.sm }),
|
|
121
|
-
paddingLeft: spacing.xl,
|
|
122
|
-
paddingRight: spacing.sm,
|
|
123
|
-
backgroundColor: "transparent",
|
|
110
|
+
...styles.indicatorItem,
|
|
124
111
|
...(Platform.OS === "web" && {
|
|
125
112
|
cursor: props.disabled ? "not-allowed" : "pointer",
|
|
126
113
|
outlineStyle: "none",
|
|
@@ -142,15 +129,7 @@ function DropdownMenuCheckboxItem({ children, style: styleOverride, ...props })
|
|
|
142
129
|
function DropdownMenuRadioItem({ children, style: styleOverride, ...props }) {
|
|
143
130
|
const { theme } = useTheme();
|
|
144
131
|
return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsxs(DropdownMenuPrimitive.RadioItem, { ...props, style: {
|
|
145
|
-
|
|
146
|
-
flexDirection: "row",
|
|
147
|
-
alignItems: "center",
|
|
148
|
-
gap: spacing.sm,
|
|
149
|
-
borderRadius: spacing.radiusSm,
|
|
150
|
-
paddingVertical: Platform.select({ web: spacing.xs, default: spacing.sm }),
|
|
151
|
-
paddingLeft: spacing.xl,
|
|
152
|
-
paddingRight: spacing.sm,
|
|
153
|
-
backgroundColor: "transparent",
|
|
132
|
+
...styles.indicatorItem,
|
|
154
133
|
...(Platform.OS === "web" && {
|
|
155
134
|
cursor: props.disabled ? "not-allowed" : "pointer",
|
|
156
135
|
outlineStyle: "none",
|
|
@@ -201,7 +180,7 @@ function DropdownMenuSeparator({ style: styleOverride, ...props }) {
|
|
|
201
180
|
: {}),
|
|
202
181
|
} }));
|
|
203
182
|
}
|
|
204
|
-
function DropdownMenuShortcut({ style: styleOverride, ...props }) {
|
|
183
|
+
function DropdownMenuShortcut({ children, text, style: styleOverride, ...props }) {
|
|
205
184
|
const { theme } = useTheme();
|
|
206
185
|
return (_jsx(Text, { ...props, style: [
|
|
207
186
|
{
|
|
@@ -213,6 +192,29 @@ function DropdownMenuShortcut({ style: styleOverride, ...props }) {
|
|
|
213
192
|
userSelect: "none",
|
|
214
193
|
},
|
|
215
194
|
styleOverride,
|
|
216
|
-
] }));
|
|
195
|
+
], children: text ?? children }));
|
|
217
196
|
}
|
|
197
|
+
const styles = StyleSheet.create({
|
|
198
|
+
item: {
|
|
199
|
+
position: "relative",
|
|
200
|
+
flexDirection: "row",
|
|
201
|
+
alignItems: "center",
|
|
202
|
+
gap: spacing.sm,
|
|
203
|
+
borderRadius: spacing.radiusSm,
|
|
204
|
+
paddingHorizontal: spacing.sm,
|
|
205
|
+
paddingVertical: Platform.select({ web: spacing.xs, default: spacing.sm }),
|
|
206
|
+
backgroundColor: "transparent",
|
|
207
|
+
},
|
|
208
|
+
indicatorItem: {
|
|
209
|
+
position: "relative",
|
|
210
|
+
flexDirection: "row",
|
|
211
|
+
alignItems: "center",
|
|
212
|
+
gap: spacing.sm,
|
|
213
|
+
borderRadius: spacing.radiusSm,
|
|
214
|
+
paddingVertical: Platform.select({ web: spacing.xs, default: spacing.sm }),
|
|
215
|
+
paddingLeft: spacing.xl,
|
|
216
|
+
paddingRight: spacing.sm,
|
|
217
|
+
backgroundColor: "transparent",
|
|
218
|
+
},
|
|
219
|
+
});
|
|
218
220
|
export { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, };
|
|
@@ -26,7 +26,7 @@ import { spacing } from "../constants/spacing.js";
|
|
|
26
26
|
export function EmptyState({ icon, iconSize = 48, title, description, actionLabel, onAction, actionPreset = "default", style, children, }) {
|
|
27
27
|
const { theme } = useTheme();
|
|
28
28
|
const styles = useMemo(() => createStyles(theme), [theme]);
|
|
29
|
-
return (_jsxs(View, { style: [styles.container, style], children: [!!icon && (_jsx(View, { style: styles.iconWrapper, children: _jsx(Icon, { name: icon, size: iconSize, color: theme.colors.mutedForeground }) })), _jsx(SansSerifBoldText, { selectable: false, style: styles.title, children: title }), !!description && (_jsx(SansSerifText, { selectable: false, style: styles.description, children: description })), children, !!actionLabel && onAction && (_jsx(Button, { preset: actionPreset, onPress: onAction, style: styles.action
|
|
29
|
+
return (_jsxs(View, { style: [styles.container, style], children: [!!icon && (_jsx(View, { style: styles.iconWrapper, children: _jsx(Icon, { name: icon, size: iconSize, color: theme.colors.mutedForeground }) })), _jsx(SansSerifBoldText, { selectable: false, style: styles.title, children: title }), !!description && (_jsx(SansSerifText, { selectable: false, style: styles.description, children: description })), children, !!actionLabel && onAction && (_jsx(Button, { preset: actionPreset, onPress: onAction, text: actionLabel, style: styles.action }))] }));
|
|
30
30
|
}
|
|
31
31
|
const createStyles = (theme) => StyleSheet.create({
|
|
32
32
|
container: {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import
|
|
2
|
+
import { useMemo, useRef, useState, useCallback } from "react";
|
|
3
3
|
import { View, TextInput as RNTextInput, Pressable, StyleSheet, Platform, } from "react-native";
|
|
4
|
-
import Animated, { useSharedValue, useAnimatedStyle, withTiming, useReducedMotion, } from "react-native-reanimated";
|
|
5
4
|
import { useTheme } from "../hooks/useTheme.js";
|
|
6
5
|
import { spacing } from "../constants/spacing.js";
|
|
7
6
|
import { fontFamilies } from "../constants/fonts.js";
|
|
@@ -11,7 +10,6 @@ const CELL_HEIGHT = 40;
|
|
|
11
10
|
const CELL_FONT_SIZE = 20;
|
|
12
11
|
const CELL_FONT_WEIGHT = "600";
|
|
13
12
|
const BULLET = "\u2022";
|
|
14
|
-
const ANIM_DURATION = 60;
|
|
15
13
|
/**
|
|
16
14
|
* OTP/verification code input with individual character cells.
|
|
17
15
|
*
|
|
@@ -30,7 +28,6 @@ const ANIM_DURATION = 60;
|
|
|
30
28
|
*/
|
|
31
29
|
function InputOTP({ length = 6, value = "", onChangeText, onComplete, error = false, errorText, disabled = false, autoFocus = false, secureTextEntry = false, inputMode = "numeric", style: styleOverride, }) {
|
|
32
30
|
const { theme } = useTheme();
|
|
33
|
-
const reduceMotion = useReducedMotion();
|
|
34
31
|
const inputRef = useRef(null);
|
|
35
32
|
const [focused, setFocused] = useState(false);
|
|
36
33
|
const styles = useMemo(() => createStyles(theme), [theme]);
|
|
@@ -61,13 +58,13 @@ function InputOTP({ length = 6, value = "", onChangeText, onComplete, error = fa
|
|
|
61
58
|
return;
|
|
62
59
|
}
|
|
63
60
|
}, [value]);
|
|
64
|
-
const
|
|
61
|
+
const markOtpFocused = useCallback(() => {
|
|
65
62
|
setFocused(true);
|
|
66
63
|
}, []);
|
|
67
|
-
const
|
|
64
|
+
const markOtpBlurred = useCallback(() => {
|
|
68
65
|
setFocused(false);
|
|
69
66
|
}, []);
|
|
70
|
-
return (_jsxs(View, { style: StyleSheet.flatten([styles.container, styleOverride]), children: [_jsx(RNTextInput, { ref: inputRef, value: value, onChangeText: handleChangeText, onKeyPress: handleKeyPress, onFocus:
|
|
67
|
+
return (_jsxs(View, { style: StyleSheet.flatten([styles.container, styleOverride]), children: [_jsx(RNTextInput, { ref: inputRef, value: value, onChangeText: handleChangeText, onKeyPress: handleKeyPress, onFocus: markOtpFocused, onBlur: markOtpBlurred, maxLength: length, autoFocus: autoFocus, editable: !disabled, inputMode: inputMode, autoComplete: "one-time-code", textContentType: "oneTimeCode", caretHidden: true, style: styles.hiddenInput, accessibilityLabel: "Verification code input", importantForAccessibility: "yes" }), _jsx(View, { style: styles.cellRow, children: Array.from({ length }, (_, index) => {
|
|
71
68
|
const char = value[index] ?? "";
|
|
72
69
|
const isActive = focused && index === activeIndex;
|
|
73
70
|
const displayChar = char
|
|
@@ -75,52 +72,31 @@ function InputOTP({ length = 6, value = "", onChangeText, onComplete, error = fa
|
|
|
75
72
|
? BULLET
|
|
76
73
|
: char
|
|
77
74
|
: "";
|
|
78
|
-
return (_jsx(OTPCell, { index: index, total: length, char: displayChar, isActive: isActive, hasError: hasError, disabled: disabled, theme: theme,
|
|
79
|
-
}) }), !!errorText && (_jsx(StyledText, { style:
|
|
75
|
+
return (_jsx(OTPCell, { index: index, total: length, char: displayChar, isActive: isActive, hasError: hasError, disabled: disabled, theme: theme, onPress: focusInput }, index));
|
|
76
|
+
}) }), !!errorText && (_jsx(StyledText, { style: styles.errorText, children: errorText }))] }));
|
|
80
77
|
}
|
|
81
|
-
function OTPCell({ index, total, char, isActive, hasError, disabled, theme,
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
function OTPCell({ index, total, char, isActive, hasError, disabled, theme, onPress, }) {
|
|
79
|
+
// borderWidth is a layout property — animating it forces a JS-thread layout
|
|
80
|
+
// pass every frame. The 1↔2px and color changes read as instant for OTP
|
|
81
|
+
// cells, so compute both during render.
|
|
82
|
+
const borderWidth = isActive && !hasError ? 2 : 1;
|
|
83
|
+
const borderColor = hasError
|
|
84
84
|
? theme.colors.destructive
|
|
85
85
|
: isActive
|
|
86
86
|
? theme.colors.primary
|
|
87
|
-
: theme.colors.border
|
|
88
|
-
|
|
89
|
-
React.useEffect(() => {
|
|
90
|
-
const duration = reduceMotion ? 0 : ANIM_DURATION;
|
|
91
|
-
const targetWidth = isActive && !hasError ? 2 : 1;
|
|
92
|
-
const targetColor = hasError
|
|
93
|
-
? theme.colors.destructive
|
|
94
|
-
: isActive
|
|
95
|
-
? theme.colors.primary
|
|
96
|
-
: theme.colors.border;
|
|
97
|
-
borderWidth.value = withTiming(targetWidth, { duration });
|
|
98
|
-
borderColor.value = withTiming(targetColor, { duration });
|
|
99
|
-
}, [
|
|
100
|
-
isActive,
|
|
101
|
-
hasError,
|
|
102
|
-
theme.colors.destructive,
|
|
103
|
-
theme.colors.primary,
|
|
104
|
-
theme.colors.border,
|
|
105
|
-
reduceMotion,
|
|
106
|
-
borderWidth,
|
|
107
|
-
borderColor,
|
|
108
|
-
]);
|
|
109
|
-
const animatedStyle = useAnimatedStyle(() => ({
|
|
110
|
-
borderWidth: borderWidth.value,
|
|
111
|
-
borderColor: borderColor.value,
|
|
112
|
-
}));
|
|
113
|
-
return (_jsx(Pressable, { onPress: onPress, disabled: disabled, accessibilityRole: "button", accessibilityLabel: `Digit ${index + 1} of ${total}`, accessibilityState: { disabled }, children: _jsx(Animated.View, { style: [
|
|
87
|
+
: theme.colors.border;
|
|
88
|
+
return (_jsx(Pressable, { onPress: onPress, disabled: disabled, accessibilityRole: "button", accessibilityLabel: `Digit ${index + 1} of ${total}`, accessibilityState: { disabled }, children: _jsx(View, { style: [
|
|
114
89
|
{
|
|
115
90
|
width: CELL_WIDTH,
|
|
116
91
|
height: CELL_HEIGHT,
|
|
117
92
|
borderRadius: spacing.radiusMd,
|
|
93
|
+
borderWidth,
|
|
118
94
|
justifyContent: "center",
|
|
119
95
|
alignItems: "center",
|
|
120
96
|
backgroundColor: "transparent",
|
|
121
97
|
opacity: disabled ? 0.5 : 1,
|
|
98
|
+
borderColor,
|
|
122
99
|
},
|
|
123
|
-
animatedStyle,
|
|
124
100
|
], children: _jsx(StyledText, { selectable: false, style: {
|
|
125
101
|
fontSize: CELL_FONT_SIZE,
|
|
126
102
|
fontWeight: CELL_FONT_WEIGHT,
|