@mrmeg/expo-ui 0.27.1 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +598 -0
- package/LLM_USAGE.md +233 -35
- package/README.md +504 -103
- package/dist/components/Accordion.js +12 -6
- package/dist/components/AnimatedView.d.ts +1 -1
- package/dist/components/AnimatedView.js +5 -2
- package/dist/components/Avatar.d.ts +1 -1
- package/dist/components/Avatar.js +2 -2
- package/dist/components/Badge.js +5 -1
- package/dist/components/BottomSheet.d.ts +29 -1
- package/dist/components/BottomSheet.js +107 -30
- package/dist/components/Button.d.ts +20 -4
- package/dist/components/Button.js +60 -43
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Card.js +24 -4
- package/dist/components/Carousel.js +2 -2
- package/dist/components/Checkbox.js +51 -44
- package/dist/components/Collapsible.js +7 -3
- package/dist/components/Dialog.js +87 -16
- package/dist/components/Drawer.d.ts +1 -1
- package/dist/components/Drawer.js +65 -55
- package/dist/components/DropdownMenu.js +11 -7
- package/dist/components/EmptyState.d.ts +2 -2
- package/dist/components/EmptyState.js +5 -2
- package/dist/components/Hydrated.d.ts +31 -0
- package/dist/components/Hydrated.js +30 -0
- package/dist/components/Icon.d.ts +17 -5
- package/dist/components/Icon.js +31 -10
- package/dist/components/InputOTP.js +5 -2
- package/dist/components/Item.d.ts +103 -4
- package/dist/components/Item.js +168 -15
- package/dist/components/KeyboardAvoidingView.d.ts +1 -1
- package/dist/components/KeyboardAvoidingView.js +4 -1
- package/dist/components/Label.js +3 -2
- package/dist/components/MaxWidthContainer.js +1 -1
- package/dist/components/Notification.js +45 -27
- package/dist/components/Popover.d.ts +18 -5
- package/dist/components/Popover.js +72 -8
- package/dist/components/Progress.js +8 -6
- package/dist/components/RadioGroup.js +52 -49
- package/dist/components/Screen.d.ts +61 -0
- package/dist/components/Screen.js +62 -0
- package/dist/components/SectionHeader.d.ts +1 -1
- package/dist/components/SegmentedControl.d.ts +12 -4
- package/dist/components/SegmentedControl.js +94 -4
- package/dist/components/Select.js +21 -26
- package/dist/components/Skeleton.js +24 -10
- package/dist/components/Slider.d.ts +11 -3
- package/dist/components/Slider.js +73 -6
- package/dist/components/StatCard.d.ts +4 -2
- package/dist/components/StatCard.js +3 -1
- package/dist/components/StyledText.d.ts +7 -1
- package/dist/components/StyledText.js +5 -3
- package/dist/components/Switch.d.ts +1 -1
- package/dist/components/Switch.js +20 -30
- package/dist/components/Tabs.d.ts +2 -2
- package/dist/components/Tabs.js +13 -24
- package/dist/components/TextInput.d.ts +1 -1
- package/dist/components/TextInput.js +71 -26
- package/dist/components/Toggle.d.ts +2 -42
- package/dist/components/Toggle.js +38 -27
- package/dist/components/ToggleGroup.d.ts +1 -1
- package/dist/components/ToggleGroup.js +37 -32
- package/dist/components/Tooltip.d.ts +1 -1
- package/dist/components/Tooltip.js +9 -3
- package/dist/components/UIProvider.d.ts +22 -2
- package/dist/components/UIProvider.js +15 -3
- package/dist/components/bottomSheetDismiss.d.ts +19 -0
- package/dist/components/bottomSheetDismiss.js +44 -0
- package/dist/components/index.d.ts +46 -44
- package/dist/components/index.js +2 -0
- package/dist/components/keyboardDismiss.js +12 -4
- package/dist/components/nativeTextField.android.js +13 -9
- package/dist/constants/colors.d.ts +40 -5
- package/dist/constants/colors.js +26 -8
- package/dist/constants/fonts.d.ts +41 -2
- package/dist/constants/fonts.js +46 -7
- package/dist/constants/index.d.ts +5 -4
- package/dist/constants/index.js +1 -0
- package/dist/constants/interaction.d.ts +18 -0
- package/dist/constants/interaction.js +18 -0
- package/dist/constants/spacing.d.ts +2 -1
- package/dist/constants/spacing.js +2 -1
- package/dist/design-system.json +154 -2
- package/dist/hooks/index.d.ts +12 -7
- package/dist/hooks/index.js +5 -0
- package/dist/hooks/useDimensions.d.ts +13 -8
- package/dist/hooks/useDimensions.js +123 -48
- package/dist/hooks/useFocusVisible.d.ts +30 -0
- package/dist/hooks/useFocusVisible.js +45 -0
- package/dist/hooks/useFontStyle.d.ts +2 -2
- package/dist/hooks/useFontStyle.js +3 -2
- package/dist/hooks/useHydrated.d.ts +13 -0
- package/dist/hooks/useHydrated.js +19 -0
- package/dist/hooks/useNotificationOffset.d.ts +20 -0
- package/dist/hooks/useNotificationOffset.js +34 -0
- package/dist/hooks/useResources.d.ts +21 -20
- package/dist/hooks/useResources.js +72 -68
- package/dist/hooks/useScalePress.d.ts +8 -5
- package/dist/hooks/useScalePress.js +16 -9
- package/dist/hooks/useShape.d.ts +24 -0
- package/dist/hooks/useShape.js +24 -0
- package/dist/hooks/useStaggeredEntrance.js +9 -7
- package/dist/hooks/useTheme.d.ts +3 -3
- package/dist/hooks/useTheme.js +177 -66
- package/dist/hooks/useWindowInsets.d.ts +13 -0
- package/dist/hooks/useWindowInsets.js +23 -0
- package/dist/index.d.ts +5 -5
- package/dist/lib/composeRefs.d.ts +8 -0
- package/dist/lib/composeRefs.js +20 -0
- package/dist/lib/haptics.d.ts +12 -0
- package/dist/lib/haptics.js +21 -0
- package/dist/lib/index.d.ts +6 -5
- package/dist/lib/index.js +1 -0
- package/dist/lib/interFonts.d.ts +15 -0
- package/dist/lib/interFonts.js +1 -0
- package/dist/lib/interFonts.native.d.ts +3 -0
- package/dist/lib/interFonts.native.js +20 -0
- package/dist/lib/popoverPlacement.d.ts +36 -0
- package/dist/lib/popoverPlacement.js +29 -0
- package/dist/lib/themedStyles.d.ts +1 -1
- package/dist/lib/useAnimatedValue.d.ts +15 -0
- package/dist/lib/useAnimatedValue.js +19 -0
- package/dist/lib/webHitTarget.d.ts +40 -0
- package/dist/lib/webHitTarget.js +37 -0
- package/dist/state/feedbackStore.d.ts +24 -0
- package/dist/state/feedbackStore.js +16 -0
- package/dist/state/globalUIStore.d.ts +27 -0
- package/dist/state/globalUIStore.js +23 -0
- package/dist/state/index.d.ts +7 -5
- package/dist/state/index.js +2 -0
- package/dist/state/initialScheme.d.ts +27 -0
- package/dist/state/initialScheme.js +29 -0
- package/dist/state/notify.d.ts +1 -1
- package/dist/state/themeColorScope.d.ts +1 -1
- package/dist/state/themeStore.d.ts +69 -16
- package/dist/state/themeStore.js +65 -38
- package/llms-full.md +56 -8
- package/llms.txt +10 -1
- package/package.json +69 -2
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { type StyleProp, type ViewStyle } from "react-native";
|
|
3
3
|
import * as TabsPrimitive from "@rn-primitives/tabs";
|
|
4
|
-
import { StyledText } from "./StyledText";
|
|
5
|
-
import { type IconName } from "./Icon";
|
|
4
|
+
import { StyledText } from "./StyledText.js";
|
|
5
|
+
import { type IconName } from "./Icon.js";
|
|
6
6
|
type TabsVariant = "underline" | "pill";
|
|
7
7
|
type TabsSize = "sm" | "md";
|
|
8
8
|
export interface TabsProps extends TabsPrimitive.RootProps {
|
package/dist/components/Tabs.js
CHANGED
|
@@ -3,12 +3,15 @@ import * as React from "react";
|
|
|
3
3
|
import { Animated, Platform, StyleSheet, View } from "react-native";
|
|
4
4
|
import * as TabsPrimitive from "@rn-primitives/tabs";
|
|
5
5
|
import { StyledText } from "./StyledText.js";
|
|
6
|
-
import { TextClassContext, TextColorContext } from "./StyledText.context";
|
|
6
|
+
import { TextClassContext, TextColorContext } from "./StyledText.context.js";
|
|
7
7
|
import { Icon } from "./Icon.js";
|
|
8
8
|
import { useTheme } from "../hooks/useTheme.js";
|
|
9
|
+
import { useFocusVisible } from "../hooks/useFocusVisible.js";
|
|
9
10
|
import { useReducedMotion } from "../hooks/useReduceMotion.js";
|
|
10
11
|
import { useScalePress } from "../hooks/useScalePress.js";
|
|
12
|
+
import { useAnimatedValue } from "../lib/useAnimatedValue.js";
|
|
11
13
|
import { spacing } from "../constants/spacing.js";
|
|
14
|
+
import { shouldUseNativeDriver } from "../lib/animations.js";
|
|
12
15
|
// ============================================================================
|
|
13
16
|
// Size configs
|
|
14
17
|
// ============================================================================
|
|
@@ -65,12 +68,12 @@ function TabsTriggerInner({ icon, style, children, value, ...props }) {
|
|
|
65
68
|
// Determine selected state by comparing trigger value with root value
|
|
66
69
|
const rootContext = TabsPrimitive.useRootContext();
|
|
67
70
|
const isSelected = rootContext.value === value;
|
|
68
|
-
const activeOpacity =
|
|
71
|
+
const activeOpacity = useAnimatedValue(isSelected ? 1 : 0);
|
|
69
72
|
React.useEffect(() => {
|
|
70
73
|
Animated.timing(activeOpacity, {
|
|
71
74
|
toValue: isSelected ? 1 : 0,
|
|
72
75
|
duration: reduceMotion ? 0 : 200,
|
|
73
|
-
useNativeDriver:
|
|
76
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
74
77
|
}).start();
|
|
75
78
|
}, [isSelected, reduceMotion, activeOpacity]);
|
|
76
79
|
const textColor = isDisabled
|
|
@@ -79,32 +82,18 @@ function TabsTriggerInner({ icon, style, children, value, ...props }) {
|
|
|
79
82
|
? theme.colors.foreground
|
|
80
83
|
: theme.colors.mutedForeground;
|
|
81
84
|
const focusRingStyle = getFocusRingStyle();
|
|
82
|
-
const [focused, setFocused] = React.useState(false);
|
|
83
85
|
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
84
86
|
disabled: isDisabled,
|
|
85
87
|
scaleTo: 0.97,
|
|
86
88
|
haptic: false,
|
|
87
89
|
});
|
|
88
|
-
const showFocusRing = (
|
|
89
|
-
let ringVisible = true;
|
|
90
|
-
if (Platform.OS === "web") {
|
|
91
|
-
const target = event?.nativeEvent?.target;
|
|
92
|
-
if (target && typeof target.matches === "function") {
|
|
93
|
-
try {
|
|
94
|
-
ringVisible = target.matches(":focus-visible");
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
ringVisible = true;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
setFocused(ringVisible);
|
|
102
|
-
};
|
|
103
|
-
const hideFocusRing = () => {
|
|
104
|
-
setFocused(false);
|
|
105
|
-
};
|
|
90
|
+
const { focused, onFocus: showFocusRing, onBlur: hideFocusRing } = useFocusVisible();
|
|
106
91
|
const triggerBaseStyle = {
|
|
107
|
-
flex: 1
|
|
92
|
+
// The wrapper below shares the list's width (`flex: 1` on the row axis);
|
|
93
|
+
// the trigger only fills it. `flex: 1` here would act on the wrapper's
|
|
94
|
+
// column axis, where a 0% basis in an indefinite-height parent collapses
|
|
95
|
+
// the trigger to its text (16px tall on web, under the 36px it declares).
|
|
96
|
+
alignSelf: "stretch",
|
|
108
97
|
// Allow the trigger to shrink below its content's intrinsic width so long
|
|
109
98
|
// labels are constrained to the trigger's flex share (and can ellipsize)
|
|
110
99
|
// instead of overflowing and clipping at the screen edge.
|
|
@@ -165,7 +154,7 @@ function TabsContent({ style, children, ...props }) {
|
|
|
165
154
|
// ============================================================================
|
|
166
155
|
// Styles
|
|
167
156
|
// ============================================================================
|
|
168
|
-
const triggerContentStyles = StyleSheet.create({
|
|
157
|
+
const triggerContentStyles = /*#__PURE__*/ StyleSheet.create({
|
|
169
158
|
container: {
|
|
170
159
|
flexDirection: "row",
|
|
171
160
|
alignItems: "center",
|
|
@@ -120,7 +120,7 @@ interface TextInputCustomProps extends TextInputProps {
|
|
|
120
120
|
* // With custom elements
|
|
121
121
|
* <TextInput
|
|
122
122
|
* label="Search"
|
|
123
|
-
* leftElement={<Icon
|
|
123
|
+
* leftElement={<Icon name="search" size={20} />}
|
|
124
124
|
* />
|
|
125
125
|
* ```
|
|
126
126
|
*/
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useCallback, useImperativeHandle, useMemo, useRef, useState, } from "react";
|
|
2
|
+
import { useEffect, useCallback, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
3
3
|
import { StyleSheet, TextInput as RNTextInput, Platform, View, Pressable, } from "react-native";
|
|
4
4
|
import { Host, useNativeState, } from "@expo/ui";
|
|
5
5
|
import { NativeTextField } from "./nativeTextField";
|
|
6
6
|
import { useTheme } from "../hooks/useTheme.js";
|
|
7
|
+
import { shapeRadius, useShape } from "../hooks/useShape.js";
|
|
7
8
|
import { spacing } from "../constants/spacing.js";
|
|
9
|
+
import { interaction } from "../constants/interaction.js";
|
|
8
10
|
import { useFontStyle } from "../hooks/useFontStyle.js";
|
|
9
11
|
import { StyledText } from "./StyledText.js";
|
|
10
12
|
import { Icon } from "./Icon.js";
|
|
@@ -15,6 +17,21 @@ import { palette } from "../constants/colors.js";
|
|
|
15
17
|
import { clearKeyboardFocusedInput, setKeyboardFocusedInput, } from "./keyboardFocusRegistry.js";
|
|
16
18
|
import { useTextInputSurfaceResponder } from "./keyboardDismiss.js";
|
|
17
19
|
const NUMERIC_REGEX = /^[0-9]*$/;
|
|
20
|
+
/**
|
|
21
|
+
* Write the `@expo/ui` observable text buffer. `set()` is the React
|
|
22
|
+
* Compiler-friendly writer (`@expo/ui` >= 56.0.17); earlier releases in the
|
|
23
|
+
* peer range only have the `value` setter, which calls the same native
|
|
24
|
+
* `setValue`. Kept outside the component so the compiler doesn't see a hook
|
|
25
|
+
* result being mutated, which made it skip `NativeTextInput` entirely.
|
|
26
|
+
*/
|
|
27
|
+
function writeNativeText(state, text) {
|
|
28
|
+
if (typeof state.set === "function") {
|
|
29
|
+
state.set(text);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
state.value = text;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
18
35
|
/** Platform touch-target guideline (pt); the compact fields sit below it. */
|
|
19
36
|
const MIN_TOUCH_TARGET = 44;
|
|
20
37
|
const SIZE_CONFIGS = {
|
|
@@ -77,7 +94,7 @@ const SIZE_CONFIGS = {
|
|
|
77
94
|
* // With custom elements
|
|
78
95
|
* <TextInput
|
|
79
96
|
* label="Search"
|
|
80
|
-
* leftElement={<Icon
|
|
97
|
+
* leftElement={<Icon name="search" size={20} />}
|
|
81
98
|
* />
|
|
82
99
|
* ```
|
|
83
100
|
*/
|
|
@@ -97,6 +114,8 @@ export function TextInput(props) {
|
|
|
97
114
|
*/
|
|
98
115
|
function WebTextInput({ 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, ref, ...rest }) {
|
|
99
116
|
const { theme, getContrastingColor, getFocusRingStyle } = useTheme();
|
|
117
|
+
// Host-app `setShape({ input })`; the underlined variant stays square.
|
|
118
|
+
const inputRadius = variant === "underlined" ? undefined : shapeRadius(useShape("input"));
|
|
100
119
|
const styles = themedStyles(theme)[variant][size];
|
|
101
120
|
const inputFont = useFontStyle("regular");
|
|
102
121
|
const [focused, setFocused] = useState(false);
|
|
@@ -153,8 +172,9 @@ function WebTextInput({ variant = "outline", size = "md", label, helperText, err
|
|
|
153
172
|
const togglePasswordVisible = () => {
|
|
154
173
|
setPasswordVisible(v => !v);
|
|
155
174
|
};
|
|
156
|
-
return (_jsxs(View, { style: wrapperStyle, children: [!!label && (_jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, size: "base", fontWeight: "medium", style: styles.label, children: [label, required && _jsx(StyledText, { selectable: false, fontWeight: "bold", 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: showInputFocusRing, onBlur: hideInputFocusRing, onContentSizeChange: (e) => setContentHeight(e.nativeEvent.contentSize.height), scrollEnabled: shouldScroll, placeholderTextColor: theme.colors.textDim, style: [
|
|
175
|
+
return (_jsxs(View, { style: wrapperStyle, children: [!!label && (_jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, size: "base", fontWeight: "medium", style: styles.label, children: [label, required && _jsx(StyledText, { selectable: false, fontWeight: "bold", style: styles.required, children: " *" })] }) })), _jsxs(View, { style: [styles.wrapper, inputRadius, 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: showInputFocusRing, onBlur: hideInputFocusRing, onContentSizeChange: (e) => setContentHeight(e.nativeEvent.contentSize.height), scrollEnabled: shouldScroll, placeholderTextColor: theme.colors.textDim, style: [
|
|
157
176
|
styles.input,
|
|
177
|
+
inputRadius,
|
|
158
178
|
// Resolved through the theme store so `setFonts` overrides apply;
|
|
159
179
|
// identical to the old hardcoded regular family by default.
|
|
160
180
|
inputFont,
|
|
@@ -181,7 +201,7 @@ function WebTextInput({ variant = "outline", size = "md", label, helperText, err
|
|
|
181
201
|
}, 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: () => {
|
|
182
202
|
hapticLight();
|
|
183
203
|
onChangeText?.("");
|
|
184
|
-
}, 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, { pointerEvents: "none" }], accessibilityLabel: "Error", children: _jsx(Icon, { name: "circle-alert", size: spacing.iconSm, color: "destructive", decorative: true }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { selectable: false, size: "sm", style: [
|
|
204
|
+
}, 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", decorative: true }) })), showErrorIcon && (_jsx(View, { style: [styles.errorIcon, { pointerEvents: "none" }], accessibilityLabel: "Error", children: _jsx(Icon, { name: "circle-alert", size: spacing.iconSm, color: "destructive", decorative: true }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { selectable: false, size: "sm", style: [
|
|
185
205
|
styles.helperText,
|
|
186
206
|
hasError && styles.errorText,
|
|
187
207
|
], children: errorText || helperText }))] }));
|
|
@@ -221,6 +241,7 @@ function NativeTextInput({ variant = "outline", size = "md", label, helperText,
|
|
|
221
241
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
222
242
|
leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
223
243
|
const { theme, getContrastingColor } = useTheme();
|
|
244
|
+
const inputShape = useShape("input");
|
|
224
245
|
const styles = themedStyles(theme)[variant][size];
|
|
225
246
|
const inputFont = useFontStyle("regular");
|
|
226
247
|
const [passwordVisible, setPasswordVisible] = useState(false);
|
|
@@ -241,8 +262,13 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
241
262
|
const [handoff, setHandoff] = useState(null);
|
|
242
263
|
const handoffRef = useRef(null);
|
|
243
264
|
const isFocusedRef = useRef(false);
|
|
265
|
+
// Latest flavour for the focus/blur/toggle handlers and the handoff timer,
|
|
266
|
+
// synced after each commit (before any native event can be dispatched)
|
|
267
|
+
// instead of written during render.
|
|
244
268
|
const activeSecureRef = useRef(effectiveSecureTextEntry);
|
|
245
|
-
|
|
269
|
+
useLayoutEffect(() => {
|
|
270
|
+
activeSecureRef.current = effectiveSecureTextEntry;
|
|
271
|
+
});
|
|
246
272
|
// Taps that land while a handoff is in flight are counted and applied (by
|
|
247
273
|
// parity) once it settles.
|
|
248
274
|
const queuedTogglesRef = useRef(0);
|
|
@@ -250,7 +276,9 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
250
276
|
// iOS-only per-flavour mount generation. Bumped whenever a flavour becomes
|
|
251
277
|
// the incoming view so React mounts a FRESH native view (whose `autoFocus`
|
|
252
278
|
// will run) instead of reusing one still unmounting from the previous handoff.
|
|
253
|
-
|
|
279
|
+
// State, not a ref: it keys the rendered Host, and the bump lands in the same
|
|
280
|
+
// batch as the `passwordVisible` flip that makes that flavour incoming.
|
|
281
|
+
const [generation, setGeneration] = useState({ secure: 0, plain: 0 });
|
|
254
282
|
// Last text the JS side knows about; see handleChangeText.
|
|
255
283
|
const lastTextRef = useRef(value ?? defaultValue ?? "");
|
|
256
284
|
// Armed when the hide toggle hands focus to the SecureField (iOS); see
|
|
@@ -263,7 +291,7 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
263
291
|
// programmatic sets); typing already updated `state` natively.
|
|
264
292
|
useEffect(() => {
|
|
265
293
|
if (value !== undefined && value !== state.value) {
|
|
266
|
-
state
|
|
294
|
+
writeNativeText(state, value);
|
|
267
295
|
}
|
|
268
296
|
if (value !== undefined)
|
|
269
297
|
lastTextRef.current = value;
|
|
@@ -348,10 +376,9 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
348
376
|
restoreAfterSecureHandoffRef.current = false;
|
|
349
377
|
const outgoingSecure = activeSecureRef.current;
|
|
350
378
|
if (Platform.OS === "ios") {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
generationRef.current.secure += 1;
|
|
379
|
+
setGeneration((current) => outgoingSecure
|
|
380
|
+
? { ...current, plain: current.plain + 1 }
|
|
381
|
+
: { ...current, secure: current.secure + 1 });
|
|
355
382
|
}
|
|
356
383
|
if (Platform.OS === "ios" && isFocusedRef.current) {
|
|
357
384
|
const timer = setTimeout(() => {
|
|
@@ -377,7 +404,11 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
377
404
|
}
|
|
378
405
|
setPasswordVisible((v) => !v);
|
|
379
406
|
}, [activeInput, finishHandoff, focusRegistryToken, parentOnBlur]);
|
|
380
|
-
|
|
407
|
+
// `finishHandoff` replays queued taps through this ref (it is declared
|
|
408
|
+
// before the toggle it calls). Synced after commit, like `activeSecureRef`.
|
|
409
|
+
useLayoutEffect(() => {
|
|
410
|
+
toggleRef.current = togglePasswordVisible;
|
|
411
|
+
}, [togglePasswordVisible]);
|
|
381
412
|
// iOS clears a secure field's existing text on the first keystroke after it
|
|
382
413
|
// becomes first responder (text it did not see typed). Right after the hide
|
|
383
414
|
// toggle hands focus to the SecureField that reads as "type one character,
|
|
@@ -396,7 +427,7 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
396
427
|
const next = wipedByKey ? previous + text : wipedByBackspace ? previous.slice(0, -1) : text;
|
|
397
428
|
lastTextRef.current = next;
|
|
398
429
|
if (wiped)
|
|
399
|
-
state
|
|
430
|
+
writeNativeText(state, next);
|
|
400
431
|
onChangeText?.(next);
|
|
401
432
|
}, [onChangeText, state]);
|
|
402
433
|
useEffect(() => {
|
|
@@ -419,13 +450,13 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
419
450
|
focus: () => activeInput()?.focus(),
|
|
420
451
|
blur: () => blurAll(),
|
|
421
452
|
clear: () => {
|
|
422
|
-
state
|
|
453
|
+
writeNativeText(state, "");
|
|
423
454
|
lastTextRef.current = "";
|
|
424
455
|
},
|
|
425
456
|
isFocused: () => activeInput()?.isFocused() ?? false,
|
|
426
457
|
setNativeProps: (props) => {
|
|
427
458
|
if (typeof props?.text === "string") {
|
|
428
|
-
state
|
|
459
|
+
writeNativeText(state, props.text);
|
|
429
460
|
lastTextRef.current = props.text;
|
|
430
461
|
}
|
|
431
462
|
},
|
|
@@ -452,9 +483,9 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
452
483
|
const surfaceStyle = {
|
|
453
484
|
backgroundColor,
|
|
454
485
|
borderColor,
|
|
455
|
-
borderRadius: variant === "underlined" ? 0 : spacing.radiusMd,
|
|
486
|
+
borderRadius: variant === "underlined" ? 0 : inputShape?.borderRadius ?? spacing.radiusMd,
|
|
456
487
|
borderWidth: variant === "outline" ? 1 : 0,
|
|
457
|
-
opacity: editable === false ?
|
|
488
|
+
opacity: editable === false ? interaction.disabledOpacity : 1,
|
|
458
489
|
overflow: "hidden",
|
|
459
490
|
};
|
|
460
491
|
// Native field: transparent, padding only. The visible surface is drawn by
|
|
@@ -503,9 +534,9 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
503
534
|
: styles.nativeHostHandoff, pointerEvents: inLayout ? "auto" : "none", accessibilityElementsHidden: !inLayout, importantForAccessibility: inLayout ? "auto" : "no-hide-descendants", children: _jsx(NativeTextField, { ...rest, autoFocus: rest.autoFocus || (handoff != null && active), ref: Platform.OS === "android" ? androidRef : secure ? secureRef : plainRef, value: state, defaultValue: defaultValue, onChangeText: handleChangeText, onFocus: () => handleFocus(secure), onBlur: () => handleBlur(secure), editable: editable, multiline: multiline, rows: rows, inputMode: inputMode, secureTextEntry: secure, placeholderTextColor: theme.colors.textDim, style: boxStyle, textStyle: textStyle }) }, Platform.OS === "android"
|
|
504
535
|
? "android"
|
|
505
536
|
: secure
|
|
506
|
-
? `secure-${
|
|
507
|
-
: `plain-${
|
|
508
|
-
}), hasSecureToggle && (_jsx(Pressable, { style: styles.nativePasswordToggle, onPress: togglePasswordVisible, accessibilityLabel: passwordVisible ? "Hide password" : "Show password", accessibilityRole: "button", children: _jsx(Icon, { name: passwordVisible ? "eye-off" : "eye", size: spacing.iconSm + 4, color: "textDim" }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { selectable: false, size: "sm", style: [styles.helperText, hasError && styles.errorText], children: errorText || helperText }))] }));
|
|
537
|
+
? `secure-${generation.secure}`
|
|
538
|
+
: `plain-${generation.plain}`));
|
|
539
|
+
}), hasSecureToggle && (_jsx(Pressable, { style: styles.nativePasswordToggle, onPress: togglePasswordVisible, accessibilityLabel: passwordVisible ? "Hide password" : "Show password", accessibilityRole: "button", children: _jsx(Icon, { name: passwordVisible ? "eye-off" : "eye", size: spacing.iconSm + 4, color: "textDim", decorative: true }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { selectable: false, size: "sm", style: [styles.helperText, hasError && styles.errorText], children: errorText || helperText }))] }));
|
|
509
540
|
}
|
|
510
541
|
const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
511
542
|
nativeHost: {
|
|
@@ -530,6 +561,10 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
|
530
561
|
},
|
|
531
562
|
nativePasswordToggle: {
|
|
532
563
|
paddingHorizontal: spacing.xs,
|
|
564
|
+
minWidth: spacing.minTarget,
|
|
565
|
+
minHeight: spacing.minTarget,
|
|
566
|
+
alignItems: "center",
|
|
567
|
+
justifyContent: "center",
|
|
533
568
|
},
|
|
534
569
|
wrapper: {
|
|
535
570
|
width: "100%",
|
|
@@ -555,7 +590,7 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
|
555
590
|
borderBottomWidth: 2,
|
|
556
591
|
},
|
|
557
592
|
disabled: {
|
|
558
|
-
opacity:
|
|
593
|
+
opacity: interaction.disabledOpacity,
|
|
559
594
|
...(Platform.OS === "web" && { cursor: "not-allowed" }),
|
|
560
595
|
},
|
|
561
596
|
error: {
|
|
@@ -597,11 +632,17 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
|
597
632
|
transform: [{ translateY: -10 }],
|
|
598
633
|
zIndex: 1,
|
|
599
634
|
},
|
|
635
|
+
// The eye/clear buttons draw a 16-20px glyph; the pressable itself is at
|
|
636
|
+
// least `spacing.minTarget` square (WCAG 2.5.8 — `hitSlop` is inert on web).
|
|
600
637
|
passwordToggle: {
|
|
601
638
|
position: "absolute",
|
|
602
|
-
right: spacing.sm,
|
|
639
|
+
right: spacing.sm - (spacing.minTarget - 20) / 2,
|
|
603
640
|
top: "50%",
|
|
604
|
-
|
|
641
|
+
minWidth: spacing.minTarget,
|
|
642
|
+
minHeight: spacing.minTarget,
|
|
643
|
+
alignItems: "center",
|
|
644
|
+
justifyContent: "center",
|
|
645
|
+
transform: [{ translateY: -spacing.minTarget / 2 }],
|
|
605
646
|
zIndex: 1,
|
|
606
647
|
...(Platform.OS === "web" && { cursor: "pointer" }),
|
|
607
648
|
},
|
|
@@ -614,9 +655,13 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
|
614
655
|
},
|
|
615
656
|
clearButton: {
|
|
616
657
|
position: "absolute",
|
|
617
|
-
right: spacing.sm,
|
|
658
|
+
right: spacing.sm - (spacing.minTarget - 20) / 2,
|
|
618
659
|
top: "50%",
|
|
619
|
-
|
|
660
|
+
minWidth: spacing.minTarget,
|
|
661
|
+
minHeight: spacing.minTarget,
|
|
662
|
+
alignItems: "center",
|
|
663
|
+
justifyContent: "center",
|
|
664
|
+
transform: [{ translateY: -spacing.minTarget / 2 }],
|
|
620
665
|
zIndex: 1,
|
|
621
666
|
...(Platform.OS === "web" && { cursor: "pointer" }),
|
|
622
667
|
},
|
|
@@ -633,7 +678,7 @@ const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
|
633
678
|
});
|
|
634
679
|
const VARIANT_KEYS = ["outline", "filled", "underlined"];
|
|
635
680
|
const SIZE_KEYS = ["sm", "md", "lg"];
|
|
636
|
-
const themedStyles = createThemedStyles((theme) => Object.fromEntries(VARIANT_KEYS.map((variant) => [
|
|
681
|
+
const themedStyles = /*#__PURE__*/ createThemedStyles((theme) => Object.fromEntries(VARIANT_KEYS.map((variant) => [
|
|
637
682
|
variant,
|
|
638
683
|
Object.fromEntries(SIZE_KEYS.map((size) => [size, createStyles(theme, variant, size)])),
|
|
639
684
|
])));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import * as TogglePrimitive from "@rn-primitives/toggle";
|
|
3
3
|
import { ViewStyle, StyleProp } from "react-native";
|
|
4
|
-
import type { IconName } from "./Icon";
|
|
4
|
+
import type { IconName } from "./Icon.js";
|
|
5
5
|
export type ToggleVariant = "default" | "outline";
|
|
6
6
|
export type ToggleSize = "sm" | "default" | "lg";
|
|
7
7
|
export type ToggleShape = "default" | "square" | "circle";
|
|
@@ -44,47 +44,7 @@ interface ToggleProps extends Omit<TogglePrimitive.RootProps, "style"> {
|
|
|
44
44
|
*/
|
|
45
45
|
style?: StyleProp<ViewStyle>;
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
* Enhanced Toggle Component
|
|
49
|
-
*
|
|
50
|
-
* Features:
|
|
51
|
-
* - 2 variants (default, outline)
|
|
52
|
-
* - 3 sizes (sm, default, lg)
|
|
53
|
-
* - 3 shapes (default, square, circle)
|
|
54
|
-
* - Loading state with spinner
|
|
55
|
-
* - Icon-only optimization
|
|
56
|
-
* - Style prop support
|
|
57
|
-
* - Full accessibility
|
|
58
|
-
*
|
|
59
|
-
* Usage:
|
|
60
|
-
* ```tsx
|
|
61
|
-
* // Basic
|
|
62
|
-
* const [pressed, setPressed] = useState(false);
|
|
63
|
-
* <Toggle pressed={pressed} onPressedChange={setPressed}>
|
|
64
|
-
* <SansSerifText>Toggle Me</SansSerifText>
|
|
65
|
-
* </Toggle>
|
|
66
|
-
*
|
|
67
|
-
* // With icon
|
|
68
|
-
* <Toggle pressed={pressed} onPressedChange={setPressed} iconOnly>
|
|
69
|
-
* <ToggleIcon name="bold" />
|
|
70
|
-
* </Toggle>
|
|
71
|
-
*
|
|
72
|
-
* // Different shapes
|
|
73
|
-
* <Toggle shape="square" pressed={pressed} onPressedChange={setPressed}>
|
|
74
|
-
* <SansSerifText>Square</SansSerifText>
|
|
75
|
-
* </Toggle>
|
|
76
|
-
*
|
|
77
|
-
* <Toggle shape="circle" iconOnly pressed={pressed} onPressedChange={setPressed}>
|
|
78
|
-
* <ToggleIcon name="bold" />
|
|
79
|
-
* </Toggle>
|
|
80
|
-
*
|
|
81
|
-
* // Loading state
|
|
82
|
-
* <Toggle loading pressed={pressed} onPressedChange={setPressed}>
|
|
83
|
-
* <SansSerifText>Loading...</SansSerifText>
|
|
84
|
-
* </Toggle>
|
|
85
|
-
* ```
|
|
86
|
-
*/
|
|
87
|
-
declare function Toggle({ variant, size, shape, loading, iconOnly, style: styleOverride, ...props }: ToggleProps): React.JSX.Element;
|
|
47
|
+
declare function Toggle({ variant, size, shape, loading, iconOnly, style: styleOverride, onPressedChange, ...props }: ToggleProps): React.JSX.Element;
|
|
88
48
|
/**
|
|
89
49
|
* ToggleIcon Component
|
|
90
50
|
* Icon wrapper for use inside Toggle buttons
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import React
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
3
|
import { Icon } from "./Icon.js";
|
|
4
|
-
import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.context";
|
|
4
|
+
import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.context.js";
|
|
5
5
|
import { useTheme } from "../hooks/useTheme.js";
|
|
6
|
+
import { useFocusVisible } from "../hooks/useFocusVisible.js";
|
|
6
7
|
import { spacing } from "../constants/spacing.js";
|
|
8
|
+
import { getWebHitInsets, WebHitTarget } from "../lib/webHitTarget.js";
|
|
9
|
+
import { interaction } from "../constants/interaction.js";
|
|
10
|
+
import { hapticSelection } from "../lib/haptics.js";
|
|
7
11
|
import { useScalePress } from "../hooks/useScalePress.js";
|
|
12
|
+
import { useItemControlLabel } from "./Item.js";
|
|
8
13
|
import * as TogglePrimitive from "@rn-primitives/toggle";
|
|
9
14
|
import { Platform, StyleSheet, ActivityIndicator, Animated } from "react-native";
|
|
10
15
|
import { palette } from "../constants/colors.js";
|
|
@@ -73,11 +78,12 @@ const TOGGLE_SIZES = {
|
|
|
73
78
|
* </Toggle>
|
|
74
79
|
* ```
|
|
75
80
|
*/
|
|
76
|
-
|
|
81
|
+
// Dev-only, once per session: the same unnamed toggle re-renders constantly.
|
|
82
|
+
let warnedIconOnly = false;
|
|
83
|
+
function Toggle({ variant = "default", size = "default", shape = "default", loading = false, iconOnly = false, style: styleOverride, onPressedChange, ...props }) {
|
|
77
84
|
const { theme, getContrastingColor, getFocusRingStyle, withAlpha } = useTheme();
|
|
78
85
|
const sizeConfig = TOGGLE_SIZES[size];
|
|
79
86
|
const focusRingStyle = getFocusRingStyle();
|
|
80
|
-
const [focused, setFocused] = useState(false);
|
|
81
87
|
// Calculate text color based on state and variant
|
|
82
88
|
const getTextColor = () => {
|
|
83
89
|
if (props.pressed) {
|
|
@@ -106,28 +112,33 @@ function Toggle({ variant = "default", size = "default", shape = "default", load
|
|
|
106
112
|
const children = props.children;
|
|
107
113
|
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
108
114
|
disabled: !!isDisabled,
|
|
109
|
-
scaleTo:
|
|
115
|
+
scaleTo: interaction.controlPressedScale,
|
|
110
116
|
haptic: false,
|
|
111
117
|
});
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
ringVisible = target.matches(":focus-visible");
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
ringVisible = true;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
setFocused(ringVisible);
|
|
126
|
-
};
|
|
127
|
-
const hideFocusRing = () => {
|
|
128
|
-
setFocused(false);
|
|
118
|
+
// A toggle is a selection control: its state change is the haptic moment,
|
|
119
|
+
// not the press, so it follows the `"selection"` setting like Switch does.
|
|
120
|
+
const handlePressedChange = (next) => {
|
|
121
|
+
hapticSelection();
|
|
122
|
+
onPressedChange?.(next);
|
|
129
123
|
};
|
|
130
|
-
|
|
124
|
+
const { focused, onFocus: showFocusRing, onBlur: hideFocusRing } = useFocusVisible();
|
|
125
|
+
const rowLabel = useItemControlLabel(props);
|
|
126
|
+
if (process.env.NODE_ENV !== "production" &&
|
|
127
|
+
iconOnly &&
|
|
128
|
+
!warnedIconOnly &&
|
|
129
|
+
props.accessibilityLabel === undefined &&
|
|
130
|
+
props["aria-label"] === undefined &&
|
|
131
|
+
props["aria-labelledby"] === undefined &&
|
|
132
|
+
rowLabel.accessibilityLabel === undefined &&
|
|
133
|
+
rowLabel["aria-labelledby"] === undefined) {
|
|
134
|
+
warnedIconOnly = true;
|
|
135
|
+
console.warn("Toggle: an icon-only toggle (`iconOnly`) has no accessible name. Pass `accessibilityLabel` " +
|
|
136
|
+
"(what the toggle turns on, e.g. \"Bold\") so screen readers announce more than \"button\".");
|
|
137
|
+
}
|
|
138
|
+
// Web pointer target: 44pt tall through a transparent child inside the drawn
|
|
139
|
+
// box (react-native-web ignores `hitSlop`); native keeps DEFAULT_HIT_SLOP.
|
|
140
|
+
const hitTarget = (_jsx(WebHitTarget, { insets: Platform.OS === "web" ? getWebHitInsets(undefined, sizeConfig.height) : null, border: styles.root.borderWidth }));
|
|
141
|
+
return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(Animated.View, { style: scaleStyle, children: _jsx(TogglePrimitive.Root, { ...rowLabel, ...props, disabled: isDisabled, onPressedChange: handlePressedChange, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, onFocus: showFocusRing, onBlur: hideFocusRing, style: {
|
|
131
142
|
...styles.root,
|
|
132
143
|
height: sizeConfig.height,
|
|
133
144
|
minWidth: sizeConfig.minWidth,
|
|
@@ -152,7 +163,7 @@ function Toggle({ variant = "default", size = "default", shape = "default", load
|
|
|
152
163
|
borderColor: theme.colors.primary,
|
|
153
164
|
}),
|
|
154
165
|
// Disabled state
|
|
155
|
-
opacity: isDisabled ?
|
|
166
|
+
opacity: isDisabled ? interaction.disabledOpacity : 1,
|
|
156
167
|
// Web-specific styles
|
|
157
168
|
...(Platform.OS === "web" && {
|
|
158
169
|
cursor: isDisabled ? "not-allowed" : "pointer",
|
|
@@ -166,13 +177,13 @@ function Toggle({ variant = "default", size = "default", shape = "default", load
|
|
|
166
177
|
selected: props.pressed,
|
|
167
178
|
disabled: !!isDisabled,
|
|
168
179
|
busy: loading,
|
|
169
|
-
}, 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 })) }) }) }) }));
|
|
180
|
+
}, children: loading ? (_jsxs(_Fragment, { children: [_jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(ActivityIndicator, { size: "small", color: textColor }) }), hitTarget] })) : typeof children === "function" ? ((state) => (_jsxs(_Fragment, { children: [_jsx(TextSelectabilityContext.Provider, { value: false, children: children(state) }), hitTarget] }))) : (_jsxs(_Fragment, { children: [_jsx(TextSelectabilityContext.Provider, { value: false, children: children }), hitTarget] })) }) }) }) }));
|
|
170
181
|
}
|
|
171
182
|
function ToggleIcon({ name, size, color }) {
|
|
172
183
|
const contextColor = React.use(TextColorContext);
|
|
173
184
|
return _jsx(Icon, { name: name, size: size || spacing.iconMd, color: color || contextColor });
|
|
174
185
|
}
|
|
175
|
-
const styles = StyleSheet.create({
|
|
186
|
+
const styles = /*#__PURE__*/ StyleSheet.create({
|
|
176
187
|
root: {
|
|
177
188
|
flexDirection: "row",
|
|
178
189
|
alignItems: "center",
|