@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
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
|
|
@@ -115,9 +115,10 @@ declare function DropdownMenuSeparator({ style: styleOverride, ...props }: Dropd
|
|
|
115
115
|
* Text component for displaying keyboard shortcuts
|
|
116
116
|
*/
|
|
117
117
|
interface DropdownMenuShortcutProps {
|
|
118
|
-
children
|
|
118
|
+
children?: React.ReactNode;
|
|
119
|
+
text?: string;
|
|
119
120
|
style?: TextStyle;
|
|
120
121
|
}
|
|
121
|
-
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;
|
|
122
123
|
export { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, };
|
|
123
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";
|
|
@@ -91,14 +91,7 @@ function DropdownMenuContent({ side, align = "start", sideOffset = 4, portalHost
|
|
|
91
91
|
function DropdownMenuItem({ inset = false, variant = "default", style: styleOverride, ...props }) {
|
|
92
92
|
const { theme } = useTheme();
|
|
93
93
|
return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(DropdownMenuPrimitive.Item, { ...props, style: {
|
|
94
|
-
|
|
95
|
-
flexDirection: "row",
|
|
96
|
-
alignItems: "center",
|
|
97
|
-
gap: spacing.sm,
|
|
98
|
-
borderRadius: spacing.radiusSm,
|
|
99
|
-
paddingHorizontal: spacing.sm,
|
|
100
|
-
paddingVertical: Platform.select({ web: spacing.xs, default: spacing.sm }),
|
|
101
|
-
backgroundColor: "transparent",
|
|
94
|
+
...styles.item,
|
|
102
95
|
...(Platform.OS === "web" && {
|
|
103
96
|
cursor: props.disabled ? "not-allowed" : "pointer",
|
|
104
97
|
outlineStyle: "none",
|
|
@@ -114,15 +107,7 @@ function DropdownMenuItem({ inset = false, variant = "default", style: styleOver
|
|
|
114
107
|
function DropdownMenuCheckboxItem({ children, style: styleOverride, ...props }) {
|
|
115
108
|
const { theme } = useTheme();
|
|
116
109
|
return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsxs(DropdownMenuPrimitive.CheckboxItem, { ...props, style: {
|
|
117
|
-
|
|
118
|
-
flexDirection: "row",
|
|
119
|
-
alignItems: "center",
|
|
120
|
-
gap: spacing.sm,
|
|
121
|
-
borderRadius: spacing.radiusSm,
|
|
122
|
-
paddingVertical: Platform.select({ web: spacing.xs, default: spacing.sm }),
|
|
123
|
-
paddingLeft: spacing.xl,
|
|
124
|
-
paddingRight: spacing.sm,
|
|
125
|
-
backgroundColor: "transparent",
|
|
110
|
+
...styles.indicatorItem,
|
|
126
111
|
...(Platform.OS === "web" && {
|
|
127
112
|
cursor: props.disabled ? "not-allowed" : "pointer",
|
|
128
113
|
outlineStyle: "none",
|
|
@@ -144,15 +129,7 @@ function DropdownMenuCheckboxItem({ children, style: styleOverride, ...props })
|
|
|
144
129
|
function DropdownMenuRadioItem({ children, style: styleOverride, ...props }) {
|
|
145
130
|
const { theme } = useTheme();
|
|
146
131
|
return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsxs(DropdownMenuPrimitive.RadioItem, { ...props, style: {
|
|
147
|
-
|
|
148
|
-
flexDirection: "row",
|
|
149
|
-
alignItems: "center",
|
|
150
|
-
gap: spacing.sm,
|
|
151
|
-
borderRadius: spacing.radiusSm,
|
|
152
|
-
paddingVertical: Platform.select({ web: spacing.xs, default: spacing.sm }),
|
|
153
|
-
paddingLeft: spacing.xl,
|
|
154
|
-
paddingRight: spacing.sm,
|
|
155
|
-
backgroundColor: "transparent",
|
|
132
|
+
...styles.indicatorItem,
|
|
156
133
|
...(Platform.OS === "web" && {
|
|
157
134
|
cursor: props.disabled ? "not-allowed" : "pointer",
|
|
158
135
|
outlineStyle: "none",
|
|
@@ -203,7 +180,7 @@ function DropdownMenuSeparator({ style: styleOverride, ...props }) {
|
|
|
203
180
|
: {}),
|
|
204
181
|
} }));
|
|
205
182
|
}
|
|
206
|
-
function DropdownMenuShortcut({ style: styleOverride, ...props }) {
|
|
183
|
+
function DropdownMenuShortcut({ children, text, style: styleOverride, ...props }) {
|
|
207
184
|
const { theme } = useTheme();
|
|
208
185
|
return (_jsx(Text, { ...props, style: [
|
|
209
186
|
{
|
|
@@ -215,6 +192,29 @@ function DropdownMenuShortcut({ style: styleOverride, ...props }) {
|
|
|
215
192
|
userSelect: "none",
|
|
216
193
|
},
|
|
217
194
|
styleOverride,
|
|
218
|
-
] }));
|
|
195
|
+
], children: text ?? children }));
|
|
219
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
|
+
});
|
|
220
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,
|
|
@@ -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";
|