@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.
- package/LLM_USAGE.md +13 -0
- package/README.md +20 -17
- package/dist/components/Accordion.js +19 -16
- package/dist/components/Badge.js +5 -4
- package/dist/components/Button.js +84 -51
- package/dist/components/Card.js +4 -3
- package/dist/components/Checkbox.js +6 -4
- package/dist/components/Collapsible.js +15 -14
- package/dist/components/Dialog.js +6 -6
- package/dist/components/Drawer.js +5 -5
- package/dist/components/DropdownMenu.js +119 -112
- package/dist/components/EmptyState.js +5 -3
- package/dist/components/InputOTP.js +3 -3
- package/dist/components/Label.js +5 -2
- package/dist/components/Notification.js +3 -3
- package/dist/components/Popover.js +2 -2
- package/dist/components/RadioGroup.js +6 -4
- package/dist/components/Select.js +35 -25
- package/dist/components/Slider.js +34 -24
- package/dist/components/StyledText.d.ts +13 -2
- package/dist/components/StyledText.js +28 -7
- package/dist/components/Switch.js +28 -28
- package/dist/components/Tabs.js +6 -3
- package/dist/components/TextInput.js +8 -10
- package/dist/components/Toggle.js +4 -2
- package/dist/components/ToggleGroup.js +3 -2
- package/dist/components/Tooltip.js +4 -4
- package/dist/constants/colors.d.ts +4 -0
- package/dist/constants/colors.js +9 -1
- package/dist/constants/spacing.d.ts +2 -1
- package/dist/constants/spacing.js +2 -1
- package/dist/hooks/useTheme.d.ts +3 -1
- package/dist/hooks/useTheme.js +46 -18
- package/package.json +6 -7
|
@@ -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 (
|
|
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
|
|
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:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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.
|
|
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.
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
thumbX.value = withTiming(ratio *
|
|
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
|
|
69
|
+
const ratio = getValueRatio(value, min, max);
|
|
54
70
|
thumbX.value = ratio * w;
|
|
55
|
-
}, [
|
|
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:
|
|
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:
|
|
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:
|
|
164
|
+
backgroundColor: thumbBackgroundColor,
|
|
149
165
|
borderWidth: 1,
|
|
150
|
-
borderColor:
|
|
151
|
-
...(
|
|
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
|
|
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
|
|
39
|
-
xxl:
|
|
40
|
-
display:
|
|
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
|
|
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"
|
|
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
|
|
39
|
-
const
|
|
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], [
|
|
44
|
+
{ translateX: interpolate(progress.value, [0, 1], [0, thumbTravel]) },
|
|
43
45
|
],
|
|
44
46
|
}));
|
|
45
47
|
const isIOS = variant === "ios";
|
|
46
|
-
|
|
47
|
-
|
|
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.
|
|
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.
|
|
60
|
-
: withAlpha(palette.black, 0.
|
|
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
|
-
|
|
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:
|
|
105
|
+
backgroundColor: thumbBackgroundColor,
|
|
103
106
|
borderWidth: 1,
|
|
104
107
|
borderColor: thumbBorderColor,
|
|
105
108
|
justifyContent: "center",
|
|
106
109
|
alignItems: "center",
|
|
107
|
-
...(
|
|
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
|
-
|
|
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,
|
package/dist/components/Tabs.js
CHANGED
|
@@ -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.
|
|
101
|
+
? theme.colors.ring
|
|
102
102
|
: forceLight
|
|
103
103
|
? "#d1d5db"
|
|
104
|
-
: theme.colors.
|
|
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 })) : (
|
|
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);
|