@mrmeg/expo-ui 0.1.5 → 0.1.6

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.
@@ -3,10 +3,9 @@ import * as React from "react";
3
3
  import { Platform, StyleSheet, View } from "react-native";
4
4
  import { Icon } from "./Icon.js";
5
5
  import { AnimatedView } from "./AnimatedView.js";
6
- import { TextClassContext, TextColorContext } from "./StyledText.js";
6
+ import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.js";
7
7
  import { useTheme } from "../hooks/useTheme.js";
8
8
  import { spacing } from "../constants/spacing.js";
9
- import { palette } from "../constants/colors.js";
10
9
  import * as SelectPrimitive from "@rn-primitives/select";
11
10
  import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
12
11
  import { useSafeAreaInsets } from "react-native-safe-area-context";
@@ -37,7 +36,7 @@ const SIZE_CONFIGS = {
37
36
  function SelectTrigger({ size = "md", error = false, children, style: styleOverride, disabled, ...props }) {
38
37
  const { theme } = useTheme();
39
38
  const sizeConfig = SIZE_CONFIGS[size];
40
- return (_jsxs(SelectPrimitive.Trigger, { disabled: disabled, ...props, style: {
39
+ return (_jsx(SelectPrimitive.Trigger, { disabled: disabled, ...props, style: {
41
40
  flexDirection: "row",
42
41
  justifyContent: "space-between",
43
42
  alignItems: "center",
@@ -50,12 +49,13 @@ function SelectTrigger({ size = "md", error = false, children, style: styleOverr
50
49
  ...(Platform.OS === "web" && {
51
50
  cursor: disabled ? "not-allowed" : "pointer",
52
51
  outlineStyle: "none",
52
+ userSelect: "none",
53
53
  }),
54
54
  ...(disabled && { opacity: 0.5 }),
55
55
  ...(styleOverride && typeof styleOverride !== "function"
56
56
  ? StyleSheet.flatten(styleOverride)
57
57
  : {}),
58
- }, children: [typeof children === "function" ? null : children, _jsx(Icon, { name: "chevron-down", size: 16, color: theme.colors.mutedForeground })] }));
58
+ }, children: _jsx(TextColorContext.Provider, { value: theme.colors.text, children: _jsxs(TextSelectabilityContext.Provider, { value: false, children: [typeof children === "function" ? null : children, _jsx(Icon, { name: "chevron-down", size: 16, color: theme.colors.mutedForeground })] }) }) }));
59
59
  }
60
60
  function SelectValue({ size = "md", placeholder, style: styleOverride, ...props }) {
61
61
  const { theme } = useTheme();
@@ -64,39 +64,43 @@ function SelectValue({ size = "md", placeholder, style: styleOverride, ...props
64
64
  fontSize: sizeConfig.fontSize,
65
65
  color: theme.colors.text,
66
66
  flex: 1,
67
+ userSelect: "none",
67
68
  ...(styleOverride && typeof styleOverride !== "function"
68
69
  ? StyleSheet.flatten(styleOverride)
69
70
  : {}),
70
71
  } }));
71
72
  }
72
73
  function SelectContent({ side, align = "start", sideOffset = 4, portalHost, style: styleOverride, ...props }) {
73
- const { theme, getShadowStyle, getContrastingColor } = useTheme();
74
+ const { theme, getShadowStyle } = useTheme();
74
75
  const shadowStyle = StyleSheet.flatten(getShadowStyle("soft"));
75
76
  const insets = useSafeAreaInsets();
76
- const textColor = getContrastingColor(theme.colors.background, palette.white, palette.black);
77
77
  return (_jsx(SelectPrimitive.Portal, { hostName: portalHost, children: _jsx(FullWindowOverlay, { children: _jsx(SelectPrimitive.Overlay, { style: Platform.select({
78
78
  native: StyleSheet.absoluteFill,
79
79
  default: undefined,
80
- }), children: _jsx(AnimatedView, { type: "fade", children: _jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(SelectPrimitive.Content, { side: side, align: align, sideOffset: sideOffset, insets: insets, avoidCollisions: true, ...props, style: {
81
- backgroundColor: theme.colors.background,
82
- borderWidth: 1,
83
- borderColor: theme.colors.border,
84
- borderRadius: spacing.radiusSm,
85
- padding: spacing.xs,
86
- minWidth: 128,
87
- overflow: "hidden",
88
- ...shadowStyle,
89
- ...(Platform.OS === "web" && {
90
- zIndex: 50,
91
- cursor: "default",
92
- }),
93
- ...(styleOverride && typeof styleOverride !== "function"
94
- ? StyleSheet.flatten(styleOverride)
95
- : {}),
96
- } }) }) }) }) }) }) }));
80
+ }), children: _jsx(AnimatedView, { type: "fade", children: _jsx(TextColorContext.Provider, { value: theme.colors.popoverForeground, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(SelectPrimitive.Content, { side: side, align: align, sideOffset: sideOffset, insets: insets, avoidCollisions: true, ...props, style: {
81
+ backgroundColor: theme.colors.popover,
82
+ borderWidth: 1,
83
+ borderColor: theme.colors.border,
84
+ borderRadius: spacing.radiusSm,
85
+ padding: spacing.xs,
86
+ minWidth: 128,
87
+ overflow: "hidden",
88
+ ...shadowStyle,
89
+ ...(Platform.OS === "web" && {
90
+ zIndex: 50,
91
+ cursor: "default",
92
+ userSelect: "none",
93
+ }),
94
+ ...(styleOverride && typeof styleOverride !== "function"
95
+ ? StyleSheet.flatten(styleOverride)
96
+ : {}),
97
+ } }) }) }) }) }) }) }) }));
97
98
  }
98
99
  function SelectItem({ children, style: styleOverride, ...props }) {
99
100
  const { theme } = useTheme();
101
+ const shouldRenderDefaultText = children == null ||
102
+ typeof children === "string" ||
103
+ typeof children === "number";
100
104
  return (_jsx(TextClassContext.Provider, { value: "", children: _jsxs(SelectPrimitive.Item, { ...props, style: {
101
105
  position: "relative",
102
106
  flexDirection: "row",
@@ -110,6 +114,7 @@ function SelectItem({ children, style: styleOverride, ...props }) {
110
114
  ...(Platform.OS === "web" && {
111
115
  cursor: props.disabled ? "not-allowed" : "pointer",
112
116
  outlineStyle: "none",
117
+ userSelect: "none",
113
118
  }),
114
119
  ...(props.disabled && { opacity: 0.5 }),
115
120
  ...(styleOverride && typeof styleOverride !== "function"
@@ -122,7 +127,11 @@ function SelectItem({ children, style: styleOverride, ...props }) {
122
127
  width: 14,
123
128
  alignItems: "center",
124
129
  justifyContent: "center",
125
- }, children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(Icon, { name: "check", size: 16, color: theme.colors.text, ...(Platform.OS === "web" && { style: { pointerEvents: "none" } }) }) }) }), _jsx(SelectPrimitive.ItemText, {}), typeof children === "function" ? null : children] }) }));
130
+ }, children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(Icon, { name: "check", size: 16, color: theme.colors.accent, ...(Platform.OS === "web" && { style: { pointerEvents: "none" } }) }) }) }), _jsx(TextSelectabilityContext.Provider, { value: false, children: shouldRenderDefaultText ? (_jsx(SelectPrimitive.ItemText, { style: {
131
+ color: theme.colors.popoverForeground,
132
+ fontSize: 14,
133
+ lineHeight: 20,
134
+ } })) : typeof children === "function" ? null : (children) })] }) }));
126
135
  }
127
136
  function SelectGroup({ style: styleOverride, ...props }) {
128
137
  return (_jsx(SelectPrimitive.Group, { ...props, style: {
@@ -138,7 +147,8 @@ function SelectLabel({ style: styleOverride, ...props }) {
138
147
  paddingVertical: Platform.select({ web: spacing.xs, default: spacing.sm }),
139
148
  fontSize: 14,
140
149
  fontWeight: "500",
141
- color: theme.colors.text,
150
+ color: theme.colors.popoverForeground,
151
+ userSelect: "none",
142
152
  ...(styleOverride && typeof styleOverride !== "function"
143
153
  ? StyleSheet.flatten(styleOverride)
144
154
  : {}),
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { palette } from "../constants/colors.js";
3
3
  import { useTheme } from "../hooks/useTheme.js";
4
4
  import { hapticLight } from "../lib/haptics.js";
5
- import { useCallback, useRef } from "react";
5
+ import { useCallback, useEffect, useRef } from "react";
6
6
  import { Platform, StyleSheet, View } from "react-native";
7
7
  import { Gesture, GestureDetector } from "react-native-gesture-handler";
8
8
  import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming, } from "react-native-reanimated";
@@ -18,9 +18,25 @@ function clampAndSnap(raw, min, max, step) {
18
18
  // Avoid floating-point drift
19
19
  return Math.round(stepped * 1e6) / 1e6;
20
20
  }
21
+ function getValueRatio(value, min, max) {
22
+ const range = max - min || 1;
23
+ return Math.min(Math.max((value - min) / range, 0), 1);
24
+ }
21
25
  function Slider({ value = 0, onValueChange, min = 0, max = 100, step = 1, size = "md", disabled = false, showValue = false, style: styleOverride, }) {
22
- const { theme } = useTheme();
26
+ const { theme, getShadowStyle, withAlpha } = useTheme();
23
27
  const dims = SIZES[size];
28
+ const inactiveTrackColor = theme.dark ? withAlpha(palette.white, 0.1) : theme.colors.muted;
29
+ const activeTrackColor = disabled
30
+ ? theme.dark
31
+ ? withAlpha(palette.white, 0.28)
32
+ : theme.colors.mutedForeground
33
+ : theme.colors.accent;
34
+ const thumbBackgroundColor = theme.dark ? theme.colors.card : theme.colors.background;
35
+ const thumbBorderColor = disabled
36
+ ? theme.dark
37
+ ? withAlpha(palette.white, 0.32)
38
+ : theme.colors.mutedForeground
39
+ : theme.colors.accent;
24
40
  // Track layout width captured via onLayout
25
41
  const trackWidth = useSharedValue(0);
26
42
  // Thumb position in pixels along the track
@@ -36,23 +52,23 @@ function Slider({ value = 0, onValueChange, min = 0, max = 100, step = 1, size =
36
52
  const jsHaptic = useCallback(() => {
37
53
  hapticLight();
38
54
  }, []);
39
- // Sync external value prop changes with animation
40
- const prevExternalValue = useRef(value);
41
- if (value !== prevExternalValue.current) {
42
- prevExternalValue.current = value;
43
- if (trackWidth.value > 0) {
44
- const ratio = (value - min) / (max - min || 1);
45
- thumbX.value = withTiming(ratio * trackWidth.value, { duration: 80 });
55
+ // Sync external value prop changes after render. Reanimated warns when shared
56
+ // values are read or written while React is rendering.
57
+ useEffect(() => {
58
+ const ratio = getValueRatio(value, min, max);
59
+ const width = trackWidth.value;
60
+ if (width > 0) {
61
+ thumbX.value = withTiming(ratio * width, { duration: 80 });
46
62
  }
47
63
  lastSnappedValue.value = value;
48
- }
64
+ }, [lastSnappedValue, max, min, thumbX, trackWidth, value]);
49
65
  const onTrackLayout = useCallback((e) => {
50
66
  const w = e.nativeEvent.layout.width;
51
67
  trackWidth.value = w;
52
68
  // Set initial thumb position without animation
53
- const ratio = (value - min) / (max - min || 1);
69
+ const ratio = getValueRatio(value, min, max);
54
70
  thumbX.value = ratio * w;
55
- }, [value, min, max]);
71
+ }, [max, min, thumbX, trackWidth, value]);
56
72
  const panGesture = Gesture.Pan()
57
73
  .enabled(!disabled)
58
74
  .onBegin((e) => {
@@ -117,7 +133,7 @@ function Slider({ value = 0, onValueChange, min = 0, max = 100, step = 1, size =
117
133
  alignItems: "center",
118
134
  },
119
135
  valueLabelStyle,
120
- ], pointerEvents: "none", children: _jsx(StyledText, { style: {
136
+ ], pointerEvents: "none", children: _jsx(StyledText, { selectable: false, style: {
121
137
  fontSize: 11,
122
138
  color: theme.colors.textDim,
123
139
  userSelect: "none",
@@ -128,13 +144,13 @@ function Slider({ value = 0, onValueChange, min = 0, max = 100, step = 1, size =
128
144
  }, onLayout: onTrackLayout, children: [_jsx(View, { style: {
129
145
  height: dims.track,
130
146
  borderRadius: dims.track / 2,
131
- backgroundColor: theme.colors.muted,
147
+ backgroundColor: inactiveTrackColor,
132
148
  overflow: "hidden",
133
149
  }, children: _jsx(Animated.View, { style: [
134
150
  {
135
151
  height: dims.track,
136
152
  borderRadius: dims.track / 2,
137
- backgroundColor: theme.colors.primary,
153
+ backgroundColor: activeTrackColor,
138
154
  },
139
155
  fillStyle,
140
156
  ] }) }), _jsx(Animated.View, { style: [
@@ -145,16 +161,10 @@ function Slider({ value = 0, onValueChange, min = 0, max = 100, step = 1, size =
145
161
  width: dims.thumb,
146
162
  height: dims.thumb,
147
163
  borderRadius: dims.thumb / 2,
148
- backgroundColor: palette.white,
164
+ backgroundColor: thumbBackgroundColor,
149
165
  borderWidth: 1,
150
- borderColor: theme.colors.border,
151
- ...(Platform.OS !== "web" && {
152
- shadowColor: "#000",
153
- shadowOffset: { width: 0, height: 1 },
154
- shadowOpacity: 0.15,
155
- shadowRadius: 2,
156
- elevation: 2,
157
- }),
166
+ borderColor: thumbBorderColor,
167
+ ...getShadowStyle("subtle"),
158
168
  },
159
169
  thumbAnimatedStyle,
160
170
  ] })] }) })] }));
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { Text as RNText, TextProps as RNTextProps } from "react-native";
2
+ import { Text as RNText, TextProps as RNTextProps, StyleProp, TextStyle } from "react-native";
3
3
  /**
4
4
  * TextClassContext provides className context for nested text components
5
5
  * Used by @rn-primitives to apply consistent styling through the component tree
@@ -10,6 +10,16 @@ export declare const TextClassContext: React.Context<string | undefined>;
10
10
  * Allows parent components (like Button) to override text color for all children
11
11
  */
12
12
  export declare const TextColorContext: React.Context<string | undefined>;
13
+ /**
14
+ * TextStyleContext allows controls such as Button to pass sizing typography to
15
+ * nested StyledText children without forcing consumers to use the `text` prop.
16
+ */
17
+ export declare const TextStyleContext: React.Context<StyleProp<TextStyle>>;
18
+ /**
19
+ * Allows interactive controls to disable text selection for nested StyledText
20
+ * without changing the package-wide default for readable content.
21
+ */
22
+ export declare const TextSelectabilityContext: React.Context<boolean | undefined>;
13
23
  /**
14
24
  * Font size variants following the DM Sans / DM Serif Display scale
15
25
  */
@@ -71,7 +81,8 @@ export type TextProps = RNTextProps & {
71
81
  * - Semantic variants (title, heading, subheading, body, caption, label)
72
82
  * - Text alignment prop
73
83
  * - Font weight options (light, regular, medium, semibold, bold)
74
- * - Text selection enabled by default (userSelect: "auto")
84
+ * - Text selection enabled by default; pass `selectable={false}` for control
85
+ * chrome such as button labels, tabs, badges, and field labels
75
86
  * - numberOfLines and ellipsizeMode support from RN TextProps
76
87
  */
77
88
  export declare const StyledText: React.ForwardRefExoticComponent<RNTextProps & {
@@ -14,6 +14,16 @@ export const TextClassContext = React.createContext(undefined);
14
14
  * Allows parent components (like Button) to override text color for all children
15
15
  */
16
16
  export const TextColorContext = React.createContext(undefined);
17
+ /**
18
+ * TextStyleContext allows controls such as Button to pass sizing typography to
19
+ * nested StyledText children without forcing consumers to use the `text` prop.
20
+ */
21
+ export const TextStyleContext = React.createContext(undefined);
22
+ /**
23
+ * Allows interactive controls to disable text selection for nested StyledText
24
+ * without changing the package-wide default for readable content.
25
+ */
26
+ export const TextSelectabilityContext = React.createContext(undefined);
17
27
  const FONT_SIZES = {
18
28
  xs: 11,
19
29
  sm: 12,
@@ -35,9 +45,9 @@ const LINE_HEIGHTS = {
35
45
  display: 40.8,
36
46
  };
37
47
  const LETTER_SPACING = {
38
- sm: 0.5,
39
- xxl: -0.3,
40
- display: -0.5,
48
+ sm: 0,
49
+ xxl: 0,
50
+ display: 0,
41
51
  };
42
52
  const SEMANTIC_CONFIGS = {
43
53
  title: { size: "xxl", weight: "semibold" },
@@ -69,14 +79,18 @@ const getFontFamilyWeight = (weight) => {
69
79
  * - Semantic variants (title, heading, subheading, body, caption, label)
70
80
  * - Text alignment prop
71
81
  * - Font weight options (light, regular, medium, semibold, bold)
72
- * - Text selection enabled by default (userSelect: "auto")
82
+ * - Text selection enabled by default; pass `selectable={false}` for control
83
+ * chrome such as button labels, tabs, badges, and field labels
73
84
  * - numberOfLines and ellipsizeMode support from RN TextProps
74
85
  */
75
86
  export const StyledText = forwardRef((props, ref) => {
76
- const { tx, text, txOptions, style, variant = "sansSerif", fontWeight, size, semantic, align, children, ...otherProps } = props;
87
+ const { tx, text, txOptions, style, variant = "sansSerif", fontWeight, size, semantic, align, selectable, children, ...otherProps } = props;
77
88
  const { theme } = useTheme();
78
89
  // Check if there's a color override from parent context (e.g., Button)
79
90
  const contextColor = React.useContext(TextColorContext);
91
+ const contextTextStyle = React.useContext(TextStyleContext);
92
+ const contextSelectable = React.useContext(TextSelectabilityContext);
93
+ const resolvedSelectable = selectable ?? contextSelectable ?? true;
80
94
  // Use context color if provided, otherwise use theme default
81
95
  const color = contextColor ?? theme.colors.text;
82
96
  // If semantic variant is provided, use its config
@@ -99,6 +113,12 @@ export const StyledText = forwardRef((props, ref) => {
99
113
  const styleHasFontSize = flatStyle && "fontSize" in flatStyle;
100
114
  const styleHasLineHeight = flatStyle && "lineHeight" in flatStyle;
101
115
  const resolvedLineHeight = styleHasFontSize && !styleHasLineHeight ? undefined : lineHeight;
116
+ const flattenedContextTextStyle = contextTextStyle
117
+ ? StyleSheet.flatten(contextTextStyle)
118
+ : undefined;
119
+ const resolvedContextTextStyle = flattenedContextTextStyle && styleHasFontSize && !styleHasLineHeight
120
+ ? { ...flattenedContextTextStyle, lineHeight: undefined }
121
+ : contextTextStyle;
102
122
  const i18nText = translateText(tx, text, txOptions);
103
123
  const content = i18nText || children;
104
124
  return (_jsx(RNText, { ref: ref, style: [
@@ -107,15 +127,16 @@ export const StyledText = forwardRef((props, ref) => {
107
127
  fontFamily,
108
128
  fontSize,
109
129
  ...(resolvedLineHeight !== undefined && { lineHeight: resolvedLineHeight }),
110
- userSelect: "auto", // Changed from "none" to allow text selection
130
+ userSelect: resolvedSelectable ? "auto" : "none",
111
131
  ...(letterSpacing !== undefined && { letterSpacing }),
112
132
  ...(align && { textAlign: align }),
113
133
  },
134
+ resolvedContextTextStyle,
114
135
  style,
115
136
  // When a parent (Button, ToggleGroupItem) sets TextColorContext,
116
137
  // that color must win over any color in the style prop
117
138
  contextColor != null && { color: contextColor },
118
- ], ...otherProps, children: content }));
139
+ ], selectable: resolvedSelectable, ...otherProps, children: content }));
119
140
  });
120
141
  StyledText.displayName = "StyledText";
121
142
  /**
@@ -11,7 +11,7 @@ import Animated, { useSharedValue, useAnimatedStyle, withTiming, interpolate, us
11
11
  import { StyledText } from "./StyledText.js";
12
12
  const DEFAULT_HIT_SLOP = 8;
13
13
  function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, height: 24 }, thumbSize = 20, loading = false, style: styleOverride, ...props }) {
14
- const { theme, getContrastingColor, withAlpha } = useTheme();
14
+ const { theme, getContrastingColor, getShadowStyle, withAlpha } = useTheme();
15
15
  const reduceMotion = useReducedMotion();
16
16
  const hasMounted = useRef(false);
17
17
  // Fire haptic on user-initiated toggles (skip initial mount)
@@ -34,30 +34,29 @@ function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, he
34
34
  progress.value = withTiming(target, { duration: 120 });
35
35
  }
36
36
  }, [props.checked, reduceMotion]);
37
- // Thumb slides from left to right
38
- const thumbOffset = 2;
39
- const thumbEnd = size.width - thumbSize - thumbOffset;
37
+ // Thumb slides from left to right with equal inset on every side.
38
+ const thumbInset = Math.max(2, (size.height - thumbSize) / 2);
39
+ const thumbTravel = Math.max(0, size.width - thumbSize - thumbInset * 2);
40
+ const labelGap = spacing.xs;
41
+ const labelHorizontalInset = spacing.xs;
40
42
  const thumbAnimatedStyle = useAnimatedStyle(() => ({
41
43
  transform: [
42
- { translateX: interpolate(progress.value, [0, 1], [thumbOffset, thumbEnd]) },
44
+ { translateX: interpolate(progress.value, [0, 1], [0, thumbTravel]) },
43
45
  ],
44
46
  }));
45
47
  const isIOS = variant === "ios";
46
- // Keep the default checked state on a stable dark neutral so the white thumb
47
- // stays distinct in both light and dark themes.
48
- const checkedColor = isIOS ? "#34C759" : palette.gray900;
49
- const uncheckedColor = theme.dark ? withAlpha(palette.white, 0.18) : palette.gray200;
48
+ const checkedColor = isIOS ? "#34C759" : theme.colors.accent;
49
+ const uncheckedColor = theme.dark ? withAlpha(palette.white, 0.12) : theme.colors.input;
50
50
  const trackBg = props.checked ? checkedColor : uncheckedColor;
51
51
  const trackBorderColor = props.checked
52
- ? theme.dark
53
- ? withAlpha(palette.white, 0.18)
54
- : withAlpha(palette.black, 0.08)
52
+ ? withAlpha(checkedColor, theme.dark ? 0.55 : 0.42)
55
53
  : theme.dark
56
- ? withAlpha(palette.white, 0.14)
54
+ ? withAlpha(palette.white, 0.16)
57
55
  : palette.gray300;
56
+ const thumbBackgroundColor = theme.dark ? palette.gray100 : palette.white;
58
57
  const thumbBorderColor = theme.dark
59
- ? withAlpha(palette.black, 0.24)
60
- : withAlpha(palette.black, 0.12);
58
+ ? withAlpha(palette.black, 0.36)
59
+ : withAlpha(palette.black, 0.1);
61
60
  const thumbIndicatorColor = props.checked ? checkedColor : theme.colors.textDim;
62
61
  // Calculate label color for ON state
63
62
  const labelOnColor = getContrastingColor(checkedColor, palette.white, palette.black);
@@ -71,7 +70,7 @@ function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, he
71
70
  borderRadius: size.height / 2,
72
71
  justifyContent: "center",
73
72
  opacity: props.disabled ? 0.5 : 1,
74
- ...(Platform.OS === "web" && { cursor: "pointer" }),
73
+ ...(Platform.OS === "web" && { cursor: props.disabled ? "not-allowed" : "pointer" }),
75
74
  ...(flattenedStyle || {}),
76
75
  }, hitSlop: DEFAULT_HIT_SLOP, accessibilityRole: "switch", accessibilityState: {
77
76
  checked: props.checked,
@@ -85,11 +84,14 @@ function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, he
85
84
  borderColor: trackBorderColor,
86
85
  }, pointerEvents: "none" }), labelOn && !isIOS && (_jsx(View, { style: {
87
86
  position: "absolute",
88
- left: spacing.sm,
87
+ top: 0,
88
+ bottom: 0,
89
+ left: labelHorizontalInset,
90
+ right: thumbInset + thumbSize + labelGap,
89
91
  justifyContent: "center",
90
92
  alignItems: "center",
91
93
  opacity: props.checked ? 1 : 0,
92
- }, pointerEvents: "none", children: _jsx(StyledText, { style: {
94
+ }, pointerEvents: "none", children: _jsx(StyledText, { selectable: false, style: {
93
95
  fontFamily: fontFamilies.sansSerif.bold,
94
96
  fontSize: labelFontSize,
95
97
  color: labelOnColor,
@@ -98,28 +100,26 @@ function Switch({ variant = "default", labelOn, labelOff, size = { width: 44, he
98
100
  {
99
101
  width: thumbSize,
100
102
  height: thumbSize,
103
+ marginLeft: thumbInset,
101
104
  borderRadius: thumbSize / 2,
102
- backgroundColor: palette.white,
105
+ backgroundColor: thumbBackgroundColor,
103
106
  borderWidth: 1,
104
107
  borderColor: thumbBorderColor,
105
108
  justifyContent: "center",
106
109
  alignItems: "center",
107
- ...(Platform.OS !== "web" && {
108
- shadowColor: "#000",
109
- shadowOffset: { width: 0, height: 1 },
110
- shadowOpacity: 0.15,
111
- shadowRadius: 2,
112
- elevation: 2,
113
- }),
110
+ ...getShadowStyle("subtle"),
114
111
  },
115
112
  thumbAnimatedStyle,
116
113
  ], children: loading && (_jsx(ActivityIndicator, { size: "small", color: thumbIndicatorColor })) }) }), labelOff && !isIOS && (_jsx(View, { style: {
117
114
  position: "absolute",
118
- right: spacing.sm,
115
+ top: 0,
116
+ bottom: 0,
117
+ left: thumbInset + thumbSize + labelGap,
118
+ right: labelHorizontalInset,
119
119
  justifyContent: "center",
120
120
  alignItems: "center",
121
121
  opacity: props.checked ? 0 : 1,
122
- }, pointerEvents: "none", children: _jsx(StyledText, { style: {
122
+ }, pointerEvents: "none", children: _jsx(StyledText, { selectable: false, style: {
123
123
  fontFamily: fontFamilies.sansSerif.bold,
124
124
  fontSize: labelFontSize,
125
125
  color: theme.colors.text,
@@ -43,6 +43,8 @@ function TabsList({ style, children, ...props }) {
43
43
  backgroundColor: theme.colors.muted,
44
44
  borderRadius: spacing.radiusMd,
45
45
  padding: 2,
46
+ borderWidth: 1,
47
+ borderColor: theme.colors.border,
46
48
  }
47
49
  : {
48
50
  flexDirection: "row",
@@ -52,7 +54,7 @@ function TabsList({ style, children, ...props }) {
52
54
  return (_jsx(TabsPrimitive.List, { style: StyleSheet.flatten([listStyle, style]), ...props, children: children }));
53
55
  }
54
56
  function TabsTriggerInner({ icon, style, children, value, ...props }) {
55
- const { theme } = useTheme();
57
+ const { theme, getShadowStyle } = useTheme();
56
58
  const { variant, size } = useTabsContext();
57
59
  const sizeConfig = SIZE_CONFIGS[size];
58
60
  const reduceMotion = useReducedMotion();
@@ -76,7 +78,7 @@ function TabsTriggerInner({ icon, style, children, value, ...props }) {
76
78
  : theme.colors.mutedForeground;
77
79
  const triggerBaseStyle = {
78
80
  flex: 1,
79
- height: sizeConfig.height,
81
+ height: Platform.OS === "web" ? sizeConfig.height : spacing.touchTarget,
80
82
  paddingHorizontal: sizeConfig.paddingHorizontal,
81
83
  flexDirection: "row",
82
84
  alignItems: "center",
@@ -88,8 +90,9 @@ function TabsTriggerInner({ icon, style, children, value, ...props }) {
88
90
  const pillActiveStyle = isSelected && variant === "pill" ? {
89
91
  backgroundColor: theme.colors.background,
90
92
  borderRadius: spacing.radiusSm,
93
+ ...getShadowStyle("subtle"),
91
94
  } : {};
92
- return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsxs(TabsPrimitive.Trigger, { value: value, style: StyleSheet.flatten([triggerBaseStyle, pillActiveStyle, style]), ...props, children: [_jsxs(View, { style: triggerContentStyles.container, children: [icon && (_jsx(Icon, { name: icon, size: sizeConfig.iconSize, color: textColor, decorative: true })), typeof children === "string" ? (_jsx(StyledText, { style: { fontSize: sizeConfig.fontSize }, children: children })) : children] }), variant === "underline" && (_jsx(Animated.View, { style: [
95
+ return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsxs(TabsPrimitive.Trigger, { value: value, style: StyleSheet.flatten([triggerBaseStyle, pillActiveStyle, style]), ...props, children: [_jsxs(View, { style: triggerContentStyles.container, children: [icon && (_jsx(Icon, { name: icon, size: sizeConfig.iconSize, color: textColor, decorative: true })), typeof children === "string" ? (_jsx(StyledText, { selectable: false, style: { fontSize: sizeConfig.fontSize }, children: children })) : children] }), variant === "underline" && (_jsx(Animated.View, { style: [
93
96
  {
94
97
  position: "absolute",
95
98
  bottom: 0,
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { useState } from "react";
2
+ import React, { useMemo, useState } from "react";
3
3
  import { StyleSheet, TextInput as RNTextInput, Platform, View, Pressable, } from "react-native";
4
4
  import { useTheme } from "../hooks/useTheme.js";
5
5
  import { spacing } from "../constants/spacing.js";
@@ -71,8 +71,8 @@ const SIZE_CONFIGS = {
71
71
  * ```
72
72
  */
73
73
  export const TextInput = React.forwardRef(({ variant = "outline", size = "md", label, helperText, errorText, error, required, rows, showSecureEntryToggle, leftElement, rightElement, clearable = false, wrapperStyle, focusedStyle, forceLight, secureTextEntry, inputMode, style, onChangeText, onFocus, onBlur, value, multiline, editable = true, ...rest }, ref) => {
74
- const { theme, getContrastingColor } = useTheme();
75
- const styles = createStyles(theme, variant, size);
74
+ const { theme, getContrastingColor, getFocusRingStyle } = useTheme();
75
+ const styles = useMemo(() => createStyles(theme, variant, size), [theme, variant, size]);
76
76
  const [focused, setFocused] = useState(false);
77
77
  const [contentHeight, setContentHeight] = useState(0);
78
78
  const [passwordVisible, setPasswordVisible] = useState(false);
@@ -98,10 +98,10 @@ export const TextInput = React.forwardRef(({ variant = "outline", size = "md", l
98
98
  const borderColor = hasError
99
99
  ? theme.colors.destructive
100
100
  : focused
101
- ? theme.colors.primary
101
+ ? theme.colors.ring
102
102
  : forceLight
103
103
  ? "#d1d5db"
104
- : theme.colors.border;
104
+ : theme.colors.input;
105
105
  const inputPaddingLeft = leftElement
106
106
  ? sizeConfig.paddingHorizontal + spacing.xl
107
107
  : sizeConfig.paddingHorizontal;
@@ -127,7 +127,7 @@ export const TextInput = React.forwardRef(({ variant = "outline", size = "md", l
127
127
  const togglePasswordVisible = () => {
128
128
  setPasswordVisible(v => !v);
129
129
  };
130
- return (_jsxs(View, { style: wrapperStyle, children: [!!label && (_jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { style: styles.label, children: [label, required && _jsx(StyledText, { style: styles.required, children: " *" })] }) })), _jsxs(View, { style: styles.wrapper, children: [leftElement && _jsx(View, { style: styles.leftElement, children: leftElement }), _jsx(RNTextInput, { ref: ref, ...rest, editable: editable, inputMode: inputMode || "text", multiline: multiline, numberOfLines: rows, secureTextEntry: secureTextEntry && !passwordVisible, onChangeText: inputMode === "numeric" ? handleNumericChange : handleTextChange, onFocus: handleFocus, onBlur: handleBlur, onContentSizeChange: (e) => setContentHeight(e.nativeEvent.contentSize.height), scrollEnabled: shouldScroll, placeholderTextColor: theme.colors.textDim, style: [
130
+ return (_jsxs(View, { style: wrapperStyle, children: [!!label && (_jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, style: styles.label, children: [label, required && _jsx(StyledText, { selectable: false, style: styles.required, children: " *" })] }) })), _jsxs(View, { style: [styles.wrapper, focused && getFocusRingStyle()], children: [leftElement && _jsx(View, { style: styles.leftElement, children: leftElement }), _jsx(RNTextInput, { ref: ref, ...rest, editable: editable, inputMode: inputMode || "text", multiline: multiline, numberOfLines: rows, secureTextEntry: secureTextEntry && !passwordVisible, onChangeText: inputMode === "numeric" ? handleNumericChange : handleTextChange, onFocus: handleFocus, onBlur: handleBlur, onContentSizeChange: (e) => setContentHeight(e.nativeEvent.contentSize.height), scrollEnabled: shouldScroll, placeholderTextColor: theme.colors.textDim, style: [
131
131
  styles.input,
132
132
  {
133
133
  backgroundColor,
@@ -143,9 +143,6 @@ export const TextInput = React.forwardRef(({ variant = "outline", size = "md", l
143
143
  variant === "filled" && styles.filled,
144
144
  style,
145
145
  focused && focusedStyle,
146
- focused && Platform.OS === "web" && {
147
- boxShadow: `0 0 0 2px ${theme.colors.background}, 0 0 0 4px ${theme.colors.primary}`,
148
- },
149
146
  isDisabled && styles.disabled,
150
147
  hasError && styles.error,
151
148
  Platform.OS === "web" && { fontSize: Math.max(sizeConfig.fontSize, 16) },
@@ -155,7 +152,7 @@ export const TextInput = React.forwardRef(({ variant = "outline", size = "md", l
155
152
  }, accessibilityLabel: "Clear input", accessibilityRole: "button", children: _jsx(Icon, { name: "x", size: spacing.iconSm, color: "textDim", decorative: true }) })), showClearButton && rightElement && !hasSecureToggle && (_jsxs(View, { style: styles.rightElements, children: [_jsx(Pressable, { onPress: () => {
156
153
  hapticLight();
157
154
  onChangeText?.("");
158
- }, accessibilityLabel: "Clear input", accessibilityRole: "button", children: _jsx(Icon, { name: "x", size: spacing.iconSm, color: "textDim", decorative: true }) }), rightElement] })), !showClearButton && rightElement && !hasSecureToggle && (_jsx(View, { style: styles.rightElement, children: rightElement })), secureTextEntry && showSecureEntryToggle && (_jsx(Pressable, { style: styles.passwordToggle, onPress: togglePasswordVisible, accessibilityLabel: passwordVisible ? "Hide password" : "Show password", accessibilityRole: "button", children: _jsx(Icon, { name: passwordVisible ? "eye-off" : "eye", size: spacing.iconSm + 4, color: "textDim" }) })), showErrorIcon && (_jsx(View, { style: styles.errorIcon, accessibilityLabel: "Error", pointerEvents: "none", children: _jsx(Icon, { name: "alert-circle", size: spacing.iconSm, color: "destructive", decorative: true }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { style: [
155
+ }, accessibilityLabel: "Clear input", accessibilityRole: "button", children: _jsx(Icon, { name: "x", size: spacing.iconSm, color: "textDim", decorative: true }) }), rightElement] })), !showClearButton && rightElement && !hasSecureToggle && (_jsx(View, { style: styles.rightElement, children: rightElement })), secureTextEntry && showSecureEntryToggle && (_jsx(Pressable, { style: styles.passwordToggle, onPress: togglePasswordVisible, accessibilityLabel: passwordVisible ? "Hide password" : "Show password", accessibilityRole: "button", children: _jsx(Icon, { name: passwordVisible ? "eye-off" : "eye", size: spacing.iconSm + 4, color: "textDim" }) })), showErrorIcon && (_jsx(View, { style: styles.errorIcon, accessibilityLabel: "Error", pointerEvents: "none", children: _jsx(Icon, { name: "alert-circle", size: spacing.iconSm, color: "destructive", decorative: true }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { selectable: false, style: [
159
156
  styles.helperText,
160
157
  hasError && styles.errorText,
161
158
  ], children: errorText || helperText }))] }));
@@ -166,6 +163,7 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
166
163
  width: "100%",
167
164
  position: "relative",
168
165
  backgroundColor: "transparent",
166
+ borderRadius: variant === "underlined" ? 0 : spacing.radiusMd,
169
167
  justifyContent: "center",
170
168
  },
171
169
  input: {
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React from "react";
3
3
  import { Icon } from "./Icon.js";
4
- import { TextClassContext, TextColorContext } from "./StyledText.js";
4
+ import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.js";
5
5
  import { useTheme } from "../hooks/useTheme.js";
6
6
  import { spacing } from "../constants/spacing.js";
7
7
  import * as TogglePrimitive from "@rn-primitives/toggle";
@@ -100,6 +100,7 @@ function Toggle({ variant = "default", size = "default", shape = "default", load
100
100
  // Flatten style override for web compatibility
101
101
  const flattenedStyle = styleOverride ? StyleSheet.flatten(styleOverride) : undefined;
102
102
  const isDisabled = props.disabled || loading;
103
+ const children = props.children;
103
104
  return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(TogglePrimitive.Root, { ...props, disabled: isDisabled, style: {
104
105
  flexDirection: "row",
105
106
  alignItems: "center",
@@ -134,6 +135,7 @@ function Toggle({ variant = "default", size = "default", shape = "default", load
134
135
  ...(Platform.OS === "web" && {
135
136
  cursor: isDisabled ? "not-allowed" : "pointer",
136
137
  transition: "all 150ms",
138
+ userSelect: "none",
137
139
  }),
138
140
  // Apply custom style override
139
141
  ...(flattenedStyle || {}),
@@ -141,7 +143,7 @@ function Toggle({ variant = "default", size = "default", shape = "default", load
141
143
  selected: props.pressed,
142
144
  disabled: !!isDisabled,
143
145
  busy: loading,
144
- }, children: loading ? (_jsx(ActivityIndicator, { size: "small", color: textColor })) : (props.children) }) }) }));
146
+ }, children: loading ? (_jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(ActivityIndicator, { size: "small", color: textColor }) })) : typeof children === "function" ? ((state) => (_jsx(TextSelectabilityContext.Provider, { value: false, children: children(state) }))) : (_jsx(TextSelectabilityContext.Provider, { value: false, children: children })) }) }) }));
145
147
  }
146
148
  function ToggleIcon({ name, size, color }) {
147
149
  const contextColor = React.useContext(TextColorContext);