@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.
Files changed (54) hide show
  1. package/LLM_USAGE.md +4 -5
  2. package/README.md +6 -6
  3. package/dist/components/Accordion.js +21 -16
  4. package/dist/components/AnimatedView.d.ts +1 -1
  5. package/dist/components/AnimatedView.js +2 -2
  6. package/dist/components/Badge.d.ts +3 -2
  7. package/dist/components/Badge.js +4 -3
  8. package/dist/components/BottomSheet.js +31 -29
  9. package/dist/components/BottomSheetKeyboard.d.ts +7 -0
  10. package/dist/components/BottomSheetKeyboard.js +35 -0
  11. package/dist/components/Button.d.ts +55 -13
  12. package/dist/components/Button.js +72 -28
  13. package/dist/components/Card.js +8 -10
  14. package/dist/components/Checkbox.js +22 -25
  15. package/dist/components/Collapsible.js +3 -7
  16. package/dist/components/Dialog.js +1 -1
  17. package/dist/components/DismissKeyboard.js +3 -3
  18. package/dist/components/Drawer.js +21 -10
  19. package/dist/components/DropdownMenu.d.ts +16 -12
  20. package/dist/components/DropdownMenu.js +32 -30
  21. package/dist/components/EmptyState.js +1 -1
  22. package/dist/components/InputOTP.js +16 -40
  23. package/dist/components/Notification.js +50 -25
  24. package/dist/components/Popover.js +1 -1
  25. package/dist/components/Progress.d.ts +2 -2
  26. package/dist/components/Progress.js +36 -34
  27. package/dist/components/RadioGroup.js +22 -20
  28. package/dist/components/Select.js +30 -20
  29. package/dist/components/Skeleton.js +6 -6
  30. package/dist/components/Slider.js +90 -97
  31. package/dist/components/StyledText.context.d.ts +6 -0
  32. package/dist/components/StyledText.context.js +5 -0
  33. package/dist/components/StyledText.d.ts +7 -58
  34. package/dist/components/StyledText.js +8 -28
  35. package/dist/components/Switch.js +30 -26
  36. package/dist/components/Tabs.d.ts +23 -3
  37. package/dist/components/Tabs.js +39 -17
  38. package/dist/components/TextInput.d.ts +6 -2
  39. package/dist/components/TextInput.js +6 -7
  40. package/dist/components/Toggle.js +12 -7
  41. package/dist/components/ToggleGroup.js +17 -11
  42. package/dist/components/Tooltip.js +1 -1
  43. package/dist/hooks/useDimensions.js +25 -26
  44. package/dist/hooks/useReduceMotion.d.ts +5 -1
  45. package/dist/hooks/useReduceMotion.js +46 -41
  46. package/dist/hooks/useResources.js +6 -1
  47. package/dist/hooks/useScalePress.d.ts +6 -5
  48. package/dist/hooks/useScalePress.js +25 -21
  49. package/dist/hooks/useStaggeredEntrance.d.ts +9 -8
  50. package/dist/hooks/useStaggeredEntrance.js +48 -21
  51. package/dist/state/themeColorScope.js +3 -3
  52. package/llms-full.md +4 -5
  53. package/llms.txt +2 -2
  54. package/package.json +1 -4
@@ -1,7 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createContext, useContext } from "react";
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 = useContext(CardContext);
34
- if (!ctx) {
35
- // Fallback for standalone usage without Card parent
36
- const { theme } = useTheme();
37
- return { theme, styles: createCardStyles(theme) };
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 = useSharedValue(checked || indeterminate ? 1 : 0);
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
- if (reduceMotion) {
26
- checkOpacity.value = withTiming(isVisuallyChecked ? 1 : 0, { duration: 0 });
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
- if (reduceMotion) {
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: checkAnimatedStyle, children: indeterminate ? (_jsx(Icon, { name: "minus", size: sizeConfig.iconSize, color: theme.colors.primaryForeground })) : (_jsx(Icon, { name: "check", size: sizeConfig.iconSize, color: theme.colors.primaryForeground })) }) }) }));
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 * as React from "react";
3
- import { Animated, Platform, StyleSheet, View } from "react-native";
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
- const { theme } = useTheme();
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.js";
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: handlePress, accessible: false, style: { flex: 1 }, children: content }));
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: handlePress, accessible: false, style: { flex: 1 }, children: content }) }));
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, useContext, useState, useReducer, useRef } from "react";
3
- import { View, Pressable, Animated, StyleSheet, Platform, Dimensions, PanResponder, } from "react-native";
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.js";
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 = useContext(DrawerContext);
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
- const parsedWidth = parseWidth(width);
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 based on initial open state
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 translateX = useRef(new Animated.Value(open ? 0 : closedPosition)).current;
154
- const backdropOpacity = useRef(new Animated.Value(open ? 1 : 0)).current;
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: React.ReactNode;
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.js";
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
- position: "relative",
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
- position: "relative",
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
- position: "relative",
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, children: actionLabel }))] }));
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 React, { useMemo, useRef, useState, useCallback } from "react";
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 handleFocus = useCallback(() => {
61
+ const markOtpFocused = useCallback(() => {
65
62
  setFocused(true);
66
63
  }, []);
67
- const handleBlur = useCallback(() => {
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: handleFocus, onBlur: handleBlur, 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) => {
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, reduceMotion: reduceMotion, onPress: focusInput }, index));
79
- }) }), !!errorText && (_jsx(StyledText, { style: [styles.errorText], children: errorText }))] }));
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, reduceMotion, onPress, }) {
82
- const borderWidth = useSharedValue(isActive && !hasError ? 2 : 1);
83
- const borderColor = useSharedValue(hasError
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
- // Update animated values when state changes
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,