@mrmeg/expo-ui 0.1.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/README.md +96 -0
- package/dist/components/Accordion.d.ts +54 -0
- package/dist/components/Accordion.js +149 -0
- package/dist/components/Alert.d.ts +30 -0
- package/dist/components/Alert.js +25 -0
- package/dist/components/AnimatedView.d.ts +55 -0
- package/dist/components/AnimatedView.js +39 -0
- package/dist/components/Badge.d.ts +23 -0
- package/dist/components/Badge.js +74 -0
- package/dist/components/BottomSheet.d.ts +74 -0
- package/dist/components/BottomSheet.js +513 -0
- package/dist/components/Button.d.ts +129 -0
- package/dist/components/Button.js +216 -0
- package/dist/components/Card.d.ts +42 -0
- package/dist/components/Card.js +126 -0
- package/dist/components/Checkbox.d.ts +39 -0
- package/dist/components/Checkbox.js +96 -0
- package/dist/components/Collapsible.d.ts +67 -0
- package/dist/components/Collapsible.js +38 -0
- package/dist/components/Dialog.d.ts +140 -0
- package/dist/components/Dialog.js +167 -0
- package/dist/components/DismissKeyboard.d.ts +15 -0
- package/dist/components/DismissKeyboard.js +13 -0
- package/dist/components/Drawer.d.ts +74 -0
- package/dist/components/Drawer.js +423 -0
- package/dist/components/DropdownMenu.d.ts +120 -0
- package/dist/components/DropdownMenu.js +211 -0
- package/dist/components/EmptyState.d.ts +42 -0
- package/dist/components/EmptyState.js +58 -0
- package/dist/components/ErrorBoundary.d.ts +53 -0
- package/dist/components/ErrorBoundary.js +75 -0
- package/dist/components/Icon.d.ts +46 -0
- package/dist/components/Icon.js +40 -0
- package/dist/components/InputOTP.d.ts +72 -0
- package/dist/components/InputOTP.js +155 -0
- package/dist/components/Label.d.ts +61 -0
- package/dist/components/Label.js +72 -0
- package/dist/components/MaxWidthContainer.d.ts +58 -0
- package/dist/components/MaxWidthContainer.js +64 -0
- package/dist/components/Notification.d.ts +26 -0
- package/dist/components/Notification.js +230 -0
- package/dist/components/Popover.d.ts +79 -0
- package/dist/components/Popover.js +91 -0
- package/dist/components/Progress.d.ts +28 -0
- package/dist/components/Progress.js +107 -0
- package/dist/components/RadioGroup.d.ts +65 -0
- package/dist/components/RadioGroup.js +142 -0
- package/dist/components/Select.d.ts +88 -0
- package/dist/components/Select.js +172 -0
- package/dist/components/Separator.d.ts +83 -0
- package/dist/components/Separator.js +85 -0
- package/dist/components/Skeleton.d.ts +68 -0
- package/dist/components/Skeleton.js +99 -0
- package/dist/components/Slider.d.ts +24 -0
- package/dist/components/Slider.js +162 -0
- package/dist/components/StatusBar.d.ts +1 -0
- package/dist/components/StatusBar.js +19 -0
- package/dist/components/StyledText.d.ts +161 -0
- package/dist/components/StyledText.js +193 -0
- package/dist/components/Switch.d.ts +44 -0
- package/dist/components/Switch.js +129 -0
- package/dist/components/Tabs.d.ts +31 -0
- package/dist/components/Tabs.js +127 -0
- package/dist/components/TextInput.d.ts +120 -0
- package/dist/components/TextInput.js +263 -0
- package/dist/components/Toggle.d.ts +106 -0
- package/dist/components/Toggle.js +150 -0
- package/dist/components/ToggleGroup.d.ts +80 -0
- package/dist/components/ToggleGroup.js +189 -0
- package/dist/components/Tooltip.d.ts +121 -0
- package/dist/components/Tooltip.js +132 -0
- package/dist/components/index.d.ts +35 -0
- package/dist/components/index.js +35 -0
- package/dist/constants/colors.d.ts +82 -0
- package/dist/constants/colors.js +116 -0
- package/dist/constants/fonts.d.ts +32 -0
- package/dist/constants/fonts.js +91 -0
- package/dist/constants/index.d.ts +3 -0
- package/dist/constants/index.js +3 -0
- package/dist/constants/spacing.d.ts +40 -0
- package/dist/constants/spacing.js +48 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/useDimensions.d.ts +19 -0
- package/dist/hooks/useDimensions.js +55 -0
- package/dist/hooks/useReduceMotion.d.ts +5 -0
- package/dist/hooks/useReduceMotion.js +64 -0
- package/dist/hooks/useResources.d.ts +12 -0
- package/dist/hooks/useResources.js +56 -0
- package/dist/hooks/useScalePress.d.ts +57 -0
- package/dist/hooks/useScalePress.js +55 -0
- package/dist/hooks/useStaggeredEntrance.d.ts +67 -0
- package/dist/hooks/useStaggeredEntrance.js +74 -0
- package/dist/hooks/useTheme.d.ts +88 -0
- package/dist/hooks/useTheme.js +328 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/lib/animations.d.ts +1 -0
- package/dist/lib/animations.js +3 -0
- package/dist/lib/haptics.d.ts +3 -0
- package/dist/lib/haptics.js +29 -0
- package/dist/lib/index.d.ts +3 -0
- package/dist/lib/index.js +3 -0
- package/dist/lib/sentry.d.ts +16 -0
- package/dist/lib/sentry.js +55 -0
- package/dist/state/globalUIStore.d.ts +30 -0
- package/dist/state/globalUIStore.js +8 -0
- package/dist/state/index.d.ts +2 -0
- package/dist/state/index.js +2 -0
- package/dist/state/themeStore.d.ts +6 -0
- package/dist/state/themeStore.js +38 -0
- package/package.json +92 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { palette } from "../constants/colors";
|
|
3
|
+
import { fontFamilies } from "../constants/fonts";
|
|
4
|
+
import { spacing } from "../constants/spacing";
|
|
5
|
+
import { useTheme } from "../hooks/useTheme";
|
|
6
|
+
import { hapticLight } from "../lib/haptics";
|
|
7
|
+
import * as SwitchPrimitives from "@rn-primitives/switch";
|
|
8
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
9
|
+
import { ActivityIndicator, Platform, StyleSheet, View } from "react-native";
|
|
10
|
+
import Animated, { useSharedValue, useAnimatedStyle, withTiming, interpolate, useReducedMotion, } from "react-native-reanimated";
|
|
11
|
+
import { StyledText } from "./StyledText";
|
|
12
|
+
const DEFAULT_HIT_SLOP = 8;
|
|
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();
|
|
15
|
+
const reduceMotion = useReducedMotion();
|
|
16
|
+
const hasMounted = useRef(false);
|
|
17
|
+
// Fire haptic on user-initiated toggles (skip initial mount)
|
|
18
|
+
const wrappedOnCheckedChange = useCallback((checked) => {
|
|
19
|
+
if (hasMounted.current)
|
|
20
|
+
hapticLight();
|
|
21
|
+
props.onCheckedChange?.(checked);
|
|
22
|
+
}, [props.onCheckedChange]);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
hasMounted.current = true;
|
|
25
|
+
}, []);
|
|
26
|
+
// Single shared value drives everything: 0 = off, 1 = on
|
|
27
|
+
const progress = useSharedValue(props.checked ? 1 : 0);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const target = props.checked ? 1 : 0;
|
|
30
|
+
if (reduceMotion) {
|
|
31
|
+
progress.value = withTiming(target, { duration: 0 });
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
progress.value = withTiming(target, { duration: 120 });
|
|
35
|
+
}
|
|
36
|
+
}, [props.checked, reduceMotion]);
|
|
37
|
+
// Thumb slides from left to right
|
|
38
|
+
const thumbOffset = 2;
|
|
39
|
+
const thumbEnd = size.width - thumbSize - thumbOffset;
|
|
40
|
+
const thumbAnimatedStyle = useAnimatedStyle(() => ({
|
|
41
|
+
transform: [
|
|
42
|
+
{ translateX: interpolate(progress.value, [0, 1], [thumbOffset, thumbEnd]) },
|
|
43
|
+
],
|
|
44
|
+
}));
|
|
45
|
+
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;
|
|
50
|
+
const trackBg = props.checked ? checkedColor : uncheckedColor;
|
|
51
|
+
const trackBorderColor = props.checked
|
|
52
|
+
? theme.dark
|
|
53
|
+
? withAlpha(palette.white, 0.18)
|
|
54
|
+
: withAlpha(palette.black, 0.08)
|
|
55
|
+
: theme.dark
|
|
56
|
+
? withAlpha(palette.white, 0.14)
|
|
57
|
+
: palette.gray300;
|
|
58
|
+
const thumbBorderColor = theme.dark
|
|
59
|
+
? withAlpha(palette.black, 0.24)
|
|
60
|
+
: withAlpha(palette.black, 0.12);
|
|
61
|
+
const thumbIndicatorColor = props.checked ? checkedColor : theme.colors.textDim;
|
|
62
|
+
// Calculate label color for ON state
|
|
63
|
+
const labelOnColor = getContrastingColor(checkedColor, palette.white, palette.black);
|
|
64
|
+
const labelFontSize = size.height / 3;
|
|
65
|
+
// Flatten style override for web compatibility
|
|
66
|
+
const flattenedStyle = styleOverride ? StyleSheet.flatten(styleOverride) : undefined;
|
|
67
|
+
return (_jsxs(SwitchPrimitives.Root, { ...props, onCheckedChange: wrappedOnCheckedChange, style: {
|
|
68
|
+
position: "relative",
|
|
69
|
+
width: size.width,
|
|
70
|
+
height: size.height,
|
|
71
|
+
borderRadius: size.height / 2,
|
|
72
|
+
justifyContent: "center",
|
|
73
|
+
opacity: props.disabled ? 0.5 : 1,
|
|
74
|
+
...(Platform.OS === "web" && { cursor: "pointer" }),
|
|
75
|
+
...(flattenedStyle || {}),
|
|
76
|
+
}, hitSlop: DEFAULT_HIT_SLOP, accessibilityRole: "switch", accessibilityState: {
|
|
77
|
+
checked: props.checked,
|
|
78
|
+
disabled: !!props.disabled,
|
|
79
|
+
busy: loading,
|
|
80
|
+
}, children: [_jsx(View, { style: {
|
|
81
|
+
...StyleSheet.absoluteFillObject,
|
|
82
|
+
borderRadius: size.height / 2,
|
|
83
|
+
backgroundColor: trackBg,
|
|
84
|
+
borderWidth: 1,
|
|
85
|
+
borderColor: trackBorderColor,
|
|
86
|
+
}, pointerEvents: "none" }), labelOn && !isIOS && (_jsx(View, { style: {
|
|
87
|
+
position: "absolute",
|
|
88
|
+
left: spacing.sm,
|
|
89
|
+
justifyContent: "center",
|
|
90
|
+
alignItems: "center",
|
|
91
|
+
opacity: props.checked ? 1 : 0,
|
|
92
|
+
}, pointerEvents: "none", children: _jsx(StyledText, { style: {
|
|
93
|
+
fontFamily: fontFamilies.sansSerif.bold,
|
|
94
|
+
fontSize: labelFontSize,
|
|
95
|
+
color: labelOnColor,
|
|
96
|
+
userSelect: "none",
|
|
97
|
+
}, children: labelOn }) })), _jsx(SwitchPrimitives.Thumb, { children: _jsx(Animated.View, { style: [
|
|
98
|
+
{
|
|
99
|
+
width: thumbSize,
|
|
100
|
+
height: thumbSize,
|
|
101
|
+
borderRadius: thumbSize / 2,
|
|
102
|
+
backgroundColor: palette.white,
|
|
103
|
+
borderWidth: 1,
|
|
104
|
+
borderColor: thumbBorderColor,
|
|
105
|
+
justifyContent: "center",
|
|
106
|
+
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
|
+
}),
|
|
114
|
+
},
|
|
115
|
+
thumbAnimatedStyle,
|
|
116
|
+
], children: loading && (_jsx(ActivityIndicator, { size: "small", color: thumbIndicatorColor })) }) }), labelOff && !isIOS && (_jsx(View, { style: {
|
|
117
|
+
position: "absolute",
|
|
118
|
+
right: spacing.sm,
|
|
119
|
+
justifyContent: "center",
|
|
120
|
+
alignItems: "center",
|
|
121
|
+
opacity: props.checked ? 0 : 1,
|
|
122
|
+
}, pointerEvents: "none", children: _jsx(StyledText, { style: {
|
|
123
|
+
fontFamily: fontFamilies.sansSerif.bold,
|
|
124
|
+
fontSize: labelFontSize,
|
|
125
|
+
color: theme.colors.text,
|
|
126
|
+
userSelect: "none",
|
|
127
|
+
}, children: labelOff }) }))] }));
|
|
128
|
+
}
|
|
129
|
+
export { Switch };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type StyleProp, type ViewStyle } from "react-native";
|
|
2
|
+
import * as TabsPrimitive from "@rn-primitives/tabs";
|
|
3
|
+
import { type IconName } from "./Icon";
|
|
4
|
+
type TabsVariant = "underline" | "pill";
|
|
5
|
+
type TabsSize = "sm" | "md";
|
|
6
|
+
export interface TabsProps extends TabsPrimitive.RootProps {
|
|
7
|
+
variant?: TabsVariant;
|
|
8
|
+
size?: TabsSize;
|
|
9
|
+
}
|
|
10
|
+
declare function TabsRoot({ variant, size, children, ...props }: TabsProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export interface TabsListProps extends TabsPrimitive.ListProps {
|
|
12
|
+
style?: StyleProp<ViewStyle>;
|
|
13
|
+
}
|
|
14
|
+
declare function TabsList({ style, children, ...props }: TabsListProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export interface TabsTriggerProps extends TabsPrimitive.TriggerProps {
|
|
16
|
+
icon?: IconName;
|
|
17
|
+
style?: StyleProp<ViewStyle>;
|
|
18
|
+
}
|
|
19
|
+
declare function TabsTriggerInner({ icon, style, children, value, ...props }: TabsTriggerProps): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
declare const TabsTrigger: typeof TabsTriggerInner;
|
|
21
|
+
export interface TabsContentProps extends TabsPrimitive.ContentProps {
|
|
22
|
+
style?: StyleProp<ViewStyle>;
|
|
23
|
+
}
|
|
24
|
+
declare function TabsContent({ style, children, ...props }: TabsContentProps): import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
declare const Tabs: typeof TabsRoot & {
|
|
26
|
+
List: typeof TabsList;
|
|
27
|
+
Trigger: typeof TabsTriggerInner;
|
|
28
|
+
Content: typeof TabsContent;
|
|
29
|
+
};
|
|
30
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent, };
|
|
31
|
+
export type { TabsVariant, TabsSize, };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { Platform, StyleSheet, View } from "react-native";
|
|
4
|
+
import * as TabsPrimitive from "@rn-primitives/tabs";
|
|
5
|
+
import Animated, { useSharedValue, useAnimatedStyle, withTiming, useReducedMotion, } from "react-native-reanimated";
|
|
6
|
+
import { StyledText, TextClassContext, TextColorContext } from "./StyledText";
|
|
7
|
+
import { Icon } from "./Icon";
|
|
8
|
+
import { useTheme } from "../hooks/useTheme";
|
|
9
|
+
import { spacing } from "../constants/spacing";
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Size configs
|
|
12
|
+
// ============================================================================
|
|
13
|
+
const SIZE_CONFIGS = {
|
|
14
|
+
sm: {
|
|
15
|
+
height: 32,
|
|
16
|
+
paddingHorizontal: spacing.sm,
|
|
17
|
+
fontSize: 12,
|
|
18
|
+
iconSize: spacing.iconSm,
|
|
19
|
+
},
|
|
20
|
+
md: {
|
|
21
|
+
height: 36,
|
|
22
|
+
paddingHorizontal: spacing.md,
|
|
23
|
+
fontSize: 13,
|
|
24
|
+
iconSize: spacing.iconMd,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const TabsContext = React.createContext({
|
|
28
|
+
variant: "underline",
|
|
29
|
+
size: "md",
|
|
30
|
+
});
|
|
31
|
+
function useTabsContext() {
|
|
32
|
+
return React.useContext(TabsContext);
|
|
33
|
+
}
|
|
34
|
+
function TabsRoot({ variant = "underline", size = "md", children, ...props }) {
|
|
35
|
+
return (_jsx(TabsContext.Provider, { value: { variant, size }, children: _jsx(TabsPrimitive.Root, { ...props, children: children }) }));
|
|
36
|
+
}
|
|
37
|
+
function TabsList({ style, children, ...props }) {
|
|
38
|
+
const { theme } = useTheme();
|
|
39
|
+
const { variant } = useTabsContext();
|
|
40
|
+
const listStyle = variant === "pill"
|
|
41
|
+
? {
|
|
42
|
+
flexDirection: "row",
|
|
43
|
+
backgroundColor: theme.colors.muted,
|
|
44
|
+
borderRadius: spacing.radiusMd,
|
|
45
|
+
padding: 2,
|
|
46
|
+
}
|
|
47
|
+
: {
|
|
48
|
+
flexDirection: "row",
|
|
49
|
+
borderBottomWidth: 1,
|
|
50
|
+
borderBottomColor: theme.colors.border,
|
|
51
|
+
};
|
|
52
|
+
return (_jsx(TabsPrimitive.List, { style: StyleSheet.flatten([listStyle, style]), ...props, children: children }));
|
|
53
|
+
}
|
|
54
|
+
function TabsTriggerInner({ icon, style, children, value, ...props }) {
|
|
55
|
+
const { theme } = useTheme();
|
|
56
|
+
const { variant, size } = useTabsContext();
|
|
57
|
+
const sizeConfig = SIZE_CONFIGS[size];
|
|
58
|
+
const reduceMotion = useReducedMotion();
|
|
59
|
+
const isDisabled = props.disabled ?? false;
|
|
60
|
+
// Determine selected state by comparing trigger value with root value
|
|
61
|
+
const rootContext = TabsPrimitive.useRootContext();
|
|
62
|
+
const isSelected = rootContext.value === value;
|
|
63
|
+
const activeOpacity = useSharedValue(isSelected ? 1 : 0);
|
|
64
|
+
React.useEffect(() => {
|
|
65
|
+
activeOpacity.value = reduceMotion
|
|
66
|
+
? (isSelected ? 1 : 0)
|
|
67
|
+
: withTiming(isSelected ? 1 : 0, { duration: 200 });
|
|
68
|
+
}, [isSelected, reduceMotion]);
|
|
69
|
+
const indicatorStyle = useAnimatedStyle(() => ({
|
|
70
|
+
opacity: activeOpacity.value,
|
|
71
|
+
}));
|
|
72
|
+
const textColor = isDisabled
|
|
73
|
+
? theme.colors.mutedForeground
|
|
74
|
+
: isSelected
|
|
75
|
+
? theme.colors.foreground
|
|
76
|
+
: theme.colors.mutedForeground;
|
|
77
|
+
const triggerBaseStyle = {
|
|
78
|
+
flex: 1,
|
|
79
|
+
height: sizeConfig.height,
|
|
80
|
+
paddingHorizontal: sizeConfig.paddingHorizontal,
|
|
81
|
+
flexDirection: "row",
|
|
82
|
+
alignItems: "center",
|
|
83
|
+
justifyContent: "center",
|
|
84
|
+
gap: spacing.xs,
|
|
85
|
+
opacity: isDisabled ? 0.5 : 1,
|
|
86
|
+
...(Platform.OS === "web" && { cursor: isDisabled ? "default" : "pointer", outlineStyle: "none" }),
|
|
87
|
+
};
|
|
88
|
+
const pillActiveStyle = isSelected && variant === "pill" ? {
|
|
89
|
+
backgroundColor: theme.colors.background,
|
|
90
|
+
borderRadius: spacing.radiusSm,
|
|
91
|
+
} : {};
|
|
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: [
|
|
93
|
+
{
|
|
94
|
+
position: "absolute",
|
|
95
|
+
bottom: 0,
|
|
96
|
+
left: 0,
|
|
97
|
+
right: 0,
|
|
98
|
+
height: 2,
|
|
99
|
+
backgroundColor: theme.colors.foreground,
|
|
100
|
+
},
|
|
101
|
+
indicatorStyle,
|
|
102
|
+
] }))] }) }) }));
|
|
103
|
+
}
|
|
104
|
+
const TabsTrigger = TabsTriggerInner;
|
|
105
|
+
function TabsContent({ style, children, ...props }) {
|
|
106
|
+
return (_jsx(TabsPrimitive.Content, { style: StyleSheet.flatten([{ marginTop: spacing.md }, style]), ...props, children: children }));
|
|
107
|
+
}
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// Styles
|
|
110
|
+
// ============================================================================
|
|
111
|
+
const triggerContentStyles = StyleSheet.create({
|
|
112
|
+
container: {
|
|
113
|
+
flexDirection: "row",
|
|
114
|
+
alignItems: "center",
|
|
115
|
+
justifyContent: "center",
|
|
116
|
+
gap: spacing.xs,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
// ============================================================================
|
|
120
|
+
// Compound Export
|
|
121
|
+
// ============================================================================
|
|
122
|
+
const Tabs = Object.assign(TabsRoot, {
|
|
123
|
+
List: TabsList,
|
|
124
|
+
Trigger: TabsTrigger,
|
|
125
|
+
Content: TabsContent,
|
|
126
|
+
});
|
|
127
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent, };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
import { TextInput as RNTextInput, ViewStyle, TextStyle, TextInputProps, StyleProp } from "react-native";
|
|
3
|
+
/**
|
|
4
|
+
* Size variants for TextInput
|
|
5
|
+
*/
|
|
6
|
+
export type TextInputSize = "sm" | "md" | "lg";
|
|
7
|
+
/**
|
|
8
|
+
* Visual variants for TextInput
|
|
9
|
+
*/
|
|
10
|
+
export type TextInputVariant = "outline" | "filled" | "underlined";
|
|
11
|
+
interface TextInputCustomProps extends TextInputProps {
|
|
12
|
+
/**
|
|
13
|
+
* Visual variant
|
|
14
|
+
* @default "outline"
|
|
15
|
+
*/
|
|
16
|
+
variant?: TextInputVariant;
|
|
17
|
+
/**
|
|
18
|
+
* Size variant
|
|
19
|
+
* @default "md"
|
|
20
|
+
*/
|
|
21
|
+
size?: TextInputSize;
|
|
22
|
+
/**
|
|
23
|
+
* Label text displayed above the input
|
|
24
|
+
*/
|
|
25
|
+
label?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Helper text displayed below the input
|
|
28
|
+
*/
|
|
29
|
+
helperText?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Error message displayed below the input (overrides helperText)
|
|
32
|
+
*/
|
|
33
|
+
errorText?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Whether the input is in an error state
|
|
36
|
+
*/
|
|
37
|
+
error?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Whether the field is required (shows asterisk)
|
|
40
|
+
*/
|
|
41
|
+
required?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Number of rows for multiline input
|
|
44
|
+
*/
|
|
45
|
+
rows?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Whether to show the password visibility toggle
|
|
48
|
+
* Only applies when secureTextEntry is true
|
|
49
|
+
*/
|
|
50
|
+
showSecureEntryToggle?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Custom element to render on the left side of the input
|
|
53
|
+
*/
|
|
54
|
+
leftElement?: ReactNode;
|
|
55
|
+
/**
|
|
56
|
+
* Custom element to render on the right side of the input
|
|
57
|
+
*/
|
|
58
|
+
rightElement?: ReactNode;
|
|
59
|
+
/**
|
|
60
|
+
* Shows an X button to clear the input when it has a value.
|
|
61
|
+
* Not shown alongside showSecureEntryToggle or on multiline inputs.
|
|
62
|
+
* @default false
|
|
63
|
+
*/
|
|
64
|
+
clearable?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Wrapper view style
|
|
67
|
+
*/
|
|
68
|
+
wrapperStyle?: StyleProp<ViewStyle>;
|
|
69
|
+
/**
|
|
70
|
+
* Style applied when input is focused
|
|
71
|
+
*/
|
|
72
|
+
focusedStyle?: StyleProp<TextStyle>;
|
|
73
|
+
/**
|
|
74
|
+
* Force light theme colors (useful for dark backgrounds)
|
|
75
|
+
*/
|
|
76
|
+
forceLight?: boolean;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Enhanced TextInput Component
|
|
80
|
+
*
|
|
81
|
+
* Features:
|
|
82
|
+
* - Size variants (sm, md, lg)
|
|
83
|
+
* - Visual variants (outline, filled, underlined)
|
|
84
|
+
* - Error states with error text
|
|
85
|
+
* - Helper text
|
|
86
|
+
* - Required indicator (asterisk)
|
|
87
|
+
* - Left/right custom elements
|
|
88
|
+
* - Password visibility toggle with eye/eye-off icons
|
|
89
|
+
* - Full accessibility support
|
|
90
|
+
* - Disabled state styling
|
|
91
|
+
*
|
|
92
|
+
* Usage:
|
|
93
|
+
* ```tsx
|
|
94
|
+
* // Basic
|
|
95
|
+
* <TextInput label="Email" placeholder="Enter email" />
|
|
96
|
+
*
|
|
97
|
+
* // With error
|
|
98
|
+
* <TextInput
|
|
99
|
+
* label="Email"
|
|
100
|
+
* error
|
|
101
|
+
* errorText="Email is required"
|
|
102
|
+
* />
|
|
103
|
+
*
|
|
104
|
+
* // With helper text
|
|
105
|
+
* <TextInput
|
|
106
|
+
* label="Password"
|
|
107
|
+
* helperText="Must be at least 8 characters"
|
|
108
|
+
* secureTextEntry
|
|
109
|
+
* showSecureEntryToggle
|
|
110
|
+
* />
|
|
111
|
+
*
|
|
112
|
+
* // With custom elements
|
|
113
|
+
* <TextInput
|
|
114
|
+
* label="Search"
|
|
115
|
+
* leftElement={<Icon as={Search} size={20} />}
|
|
116
|
+
* />
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
export declare const TextInput: React.ForwardRefExoticComponent<TextInputCustomProps & React.RefAttributes<RNTextInput>>;
|
|
120
|
+
export {};
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState } from "react";
|
|
3
|
+
import { StyleSheet, TextInput as RNTextInput, Platform, View, Pressable, } from "react-native";
|
|
4
|
+
import { useTheme } from "../hooks/useTheme";
|
|
5
|
+
import { spacing } from "../constants/spacing";
|
|
6
|
+
import { fontFamilies } from "../constants/fonts";
|
|
7
|
+
import { StyledText } from "./StyledText";
|
|
8
|
+
import { Icon } from "./Icon";
|
|
9
|
+
import { hapticLight } from "../lib/haptics";
|
|
10
|
+
import { palette } from "../constants/colors";
|
|
11
|
+
const NUMERIC_REGEX = /^[0-9]*$/;
|
|
12
|
+
const SIZE_CONFIGS = {
|
|
13
|
+
sm: {
|
|
14
|
+
height: 32,
|
|
15
|
+
fontSize: 13,
|
|
16
|
+
paddingVertical: spacing.xs,
|
|
17
|
+
paddingHorizontal: spacing.sm,
|
|
18
|
+
},
|
|
19
|
+
md: {
|
|
20
|
+
height: 36,
|
|
21
|
+
fontSize: 14,
|
|
22
|
+
paddingVertical: spacing.xs,
|
|
23
|
+
paddingHorizontal: spacing.sm,
|
|
24
|
+
},
|
|
25
|
+
lg: {
|
|
26
|
+
height: 40,
|
|
27
|
+
fontSize: 15,
|
|
28
|
+
paddingVertical: spacing.sm,
|
|
29
|
+
paddingHorizontal: spacing.md,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Enhanced TextInput Component
|
|
34
|
+
*
|
|
35
|
+
* Features:
|
|
36
|
+
* - Size variants (sm, md, lg)
|
|
37
|
+
* - Visual variants (outline, filled, underlined)
|
|
38
|
+
* - Error states with error text
|
|
39
|
+
* - Helper text
|
|
40
|
+
* - Required indicator (asterisk)
|
|
41
|
+
* - Left/right custom elements
|
|
42
|
+
* - Password visibility toggle with eye/eye-off icons
|
|
43
|
+
* - Full accessibility support
|
|
44
|
+
* - Disabled state styling
|
|
45
|
+
*
|
|
46
|
+
* Usage:
|
|
47
|
+
* ```tsx
|
|
48
|
+
* // Basic
|
|
49
|
+
* <TextInput label="Email" placeholder="Enter email" />
|
|
50
|
+
*
|
|
51
|
+
* // With error
|
|
52
|
+
* <TextInput
|
|
53
|
+
* label="Email"
|
|
54
|
+
* error
|
|
55
|
+
* errorText="Email is required"
|
|
56
|
+
* />
|
|
57
|
+
*
|
|
58
|
+
* // With helper text
|
|
59
|
+
* <TextInput
|
|
60
|
+
* label="Password"
|
|
61
|
+
* helperText="Must be at least 8 characters"
|
|
62
|
+
* secureTextEntry
|
|
63
|
+
* showSecureEntryToggle
|
|
64
|
+
* />
|
|
65
|
+
*
|
|
66
|
+
* // With custom elements
|
|
67
|
+
* <TextInput
|
|
68
|
+
* label="Search"
|
|
69
|
+
* leftElement={<Icon as={Search} size={20} />}
|
|
70
|
+
* />
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
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);
|
|
76
|
+
const [focused, setFocused] = useState(false);
|
|
77
|
+
const [contentHeight, setContentHeight] = useState(0);
|
|
78
|
+
const [passwordVisible, setPasswordVisible] = useState(false);
|
|
79
|
+
const isDisabled = editable === false;
|
|
80
|
+
const hasError = error || !!errorText;
|
|
81
|
+
// Determine background color
|
|
82
|
+
const backgroundColor = forceLight
|
|
83
|
+
? palette.white
|
|
84
|
+
: variant === "filled"
|
|
85
|
+
? theme.colors.card
|
|
86
|
+
: "transparent";
|
|
87
|
+
// Handle numeric input validation
|
|
88
|
+
const handleNumericChange = (input) => {
|
|
89
|
+
if (NUMERIC_REGEX.test(input)) {
|
|
90
|
+
onChangeText?.(input);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const handleTextChange = (input) => {
|
|
94
|
+
onChangeText?.(input);
|
|
95
|
+
};
|
|
96
|
+
const sizeConfig = SIZE_CONFIGS[size];
|
|
97
|
+
// Pre-calculate all values to avoid expensive recalculations on every keystroke
|
|
98
|
+
const borderColor = hasError
|
|
99
|
+
? theme.colors.destructive
|
|
100
|
+
: focused
|
|
101
|
+
? theme.colors.primary
|
|
102
|
+
: forceLight
|
|
103
|
+
? "#d1d5db"
|
|
104
|
+
: theme.colors.border;
|
|
105
|
+
const inputPaddingLeft = leftElement
|
|
106
|
+
? sizeConfig.paddingHorizontal + spacing.xl
|
|
107
|
+
: sizeConfig.paddingHorizontal;
|
|
108
|
+
const hasSecureToggle = !!(secureTextEntry && showSecureEntryToggle);
|
|
109
|
+
const showClearButton = clearable && !hasSecureToggle && !multiline && !isDisabled && !!value;
|
|
110
|
+
const hasRightSlot = !!rightElement || hasSecureToggle || showClearButton;
|
|
111
|
+
const showErrorIcon = hasError && !hasRightSlot && !multiline;
|
|
112
|
+
const inputPaddingRight = hasRightSlot || showErrorIcon
|
|
113
|
+
? sizeConfig.paddingHorizontal + spacing.xl
|
|
114
|
+
: sizeConfig.paddingHorizontal;
|
|
115
|
+
const textColor = forceLight
|
|
116
|
+
? "#1f2937"
|
|
117
|
+
: getContrastingColor(backgroundColor === "transparent" ? theme.colors.background : backgroundColor, theme.colors.text, palette.white);
|
|
118
|
+
const shouldScroll = multiline && rest.scrollEnabled !== false && contentHeight > 100;
|
|
119
|
+
const handleFocus = (e) => {
|
|
120
|
+
setFocused(true);
|
|
121
|
+
onFocus?.(e);
|
|
122
|
+
};
|
|
123
|
+
const handleBlur = (e) => {
|
|
124
|
+
setFocused(false);
|
|
125
|
+
onBlur?.(e);
|
|
126
|
+
};
|
|
127
|
+
const togglePasswordVisible = () => {
|
|
128
|
+
setPasswordVisible(v => !v);
|
|
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: [
|
|
131
|
+
styles.input,
|
|
132
|
+
{
|
|
133
|
+
backgroundColor,
|
|
134
|
+
borderColor,
|
|
135
|
+
color: textColor,
|
|
136
|
+
fontSize: sizeConfig.fontSize,
|
|
137
|
+
minHeight: multiline ? undefined : sizeConfig.height,
|
|
138
|
+
paddingVertical: sizeConfig.paddingVertical,
|
|
139
|
+
paddingLeft: inputPaddingLeft,
|
|
140
|
+
paddingRight: inputPaddingRight,
|
|
141
|
+
},
|
|
142
|
+
variant === "underlined" && styles.underlined,
|
|
143
|
+
variant === "filled" && styles.filled,
|
|
144
|
+
style,
|
|
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
|
+
isDisabled && styles.disabled,
|
|
150
|
+
hasError && styles.error,
|
|
151
|
+
Platform.OS === "web" && { fontSize: Math.max(sizeConfig.fontSize, 16) },
|
|
152
|
+
], textAlignVertical: multiline ? "top" : "center", value: value, accessibilityLabel: label, accessibilityHint: helperText || errorText, accessibilityState: { disabled: isDisabled }, "aria-invalid": hasError, "aria-required": required }), showClearButton && !rightElement && (_jsx(Pressable, { style: styles.clearButton, onPress: () => {
|
|
153
|
+
hapticLight();
|
|
154
|
+
onChangeText?.("");
|
|
155
|
+
}, 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
|
+
hapticLight();
|
|
157
|
+
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: [
|
|
159
|
+
styles.helperText,
|
|
160
|
+
hasError && styles.errorText,
|
|
161
|
+
], children: errorText || helperText }))] }));
|
|
162
|
+
});
|
|
163
|
+
TextInput.displayName = "TextInput";
|
|
164
|
+
const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
165
|
+
wrapper: {
|
|
166
|
+
width: "100%",
|
|
167
|
+
position: "relative",
|
|
168
|
+
backgroundColor: "transparent",
|
|
169
|
+
justifyContent: "center",
|
|
170
|
+
},
|
|
171
|
+
input: {
|
|
172
|
+
fontFamily: fontFamilies.sansSerif.regular,
|
|
173
|
+
borderRadius: spacing.radiusMd,
|
|
174
|
+
borderWidth: 1,
|
|
175
|
+
...(Platform.OS === "web" && { outlineStyle: "none" }),
|
|
176
|
+
},
|
|
177
|
+
underlined: {
|
|
178
|
+
borderRadius: 0,
|
|
179
|
+
borderWidth: 0,
|
|
180
|
+
borderBottomWidth: 2,
|
|
181
|
+
},
|
|
182
|
+
filled: {
|
|
183
|
+
borderWidth: 0,
|
|
184
|
+
borderBottomWidth: 2,
|
|
185
|
+
},
|
|
186
|
+
disabled: {
|
|
187
|
+
opacity: 0.6,
|
|
188
|
+
...(Platform.OS === "web" && { cursor: "not-allowed" }),
|
|
189
|
+
},
|
|
190
|
+
error: {
|
|
191
|
+
borderColor: theme.colors.destructive,
|
|
192
|
+
},
|
|
193
|
+
labelContainer: {
|
|
194
|
+
flexDirection: "row",
|
|
195
|
+
marginBottom: spacing.xs,
|
|
196
|
+
},
|
|
197
|
+
label: {
|
|
198
|
+
fontFamily: fontFamilies.sansSerif.regular,
|
|
199
|
+
fontWeight: "500",
|
|
200
|
+
fontSize: 14,
|
|
201
|
+
color: theme.colors.text,
|
|
202
|
+
},
|
|
203
|
+
required: {
|
|
204
|
+
color: theme.colors.destructive,
|
|
205
|
+
fontFamily: fontFamilies.sansSerif.bold,
|
|
206
|
+
},
|
|
207
|
+
helperText: {
|
|
208
|
+
fontFamily: fontFamilies.sansSerif.regular,
|
|
209
|
+
fontSize: 12,
|
|
210
|
+
color: theme.colors.textDim,
|
|
211
|
+
marginTop: spacing.xs,
|
|
212
|
+
},
|
|
213
|
+
errorText: {
|
|
214
|
+
color: theme.colors.destructive,
|
|
215
|
+
},
|
|
216
|
+
leftElement: {
|
|
217
|
+
position: "absolute",
|
|
218
|
+
left: spacing.sm,
|
|
219
|
+
top: "50%",
|
|
220
|
+
transform: [{ translateY: -10 }],
|
|
221
|
+
zIndex: 1,
|
|
222
|
+
},
|
|
223
|
+
rightElement: {
|
|
224
|
+
position: "absolute",
|
|
225
|
+
right: spacing.sm,
|
|
226
|
+
top: "50%",
|
|
227
|
+
transform: [{ translateY: -10 }],
|
|
228
|
+
zIndex: 1,
|
|
229
|
+
},
|
|
230
|
+
passwordToggle: {
|
|
231
|
+
position: "absolute",
|
|
232
|
+
right: spacing.sm,
|
|
233
|
+
top: "50%",
|
|
234
|
+
transform: [{ translateY: Platform.OS === "web" ? -10 : -12 }],
|
|
235
|
+
zIndex: 1,
|
|
236
|
+
...(Platform.OS === "web" && { cursor: "pointer" }),
|
|
237
|
+
},
|
|
238
|
+
errorIcon: {
|
|
239
|
+
position: "absolute",
|
|
240
|
+
right: spacing.sm,
|
|
241
|
+
top: "50%",
|
|
242
|
+
transform: [{ translateY: -10 }],
|
|
243
|
+
zIndex: 1,
|
|
244
|
+
},
|
|
245
|
+
clearButton: {
|
|
246
|
+
position: "absolute",
|
|
247
|
+
right: spacing.sm,
|
|
248
|
+
top: "50%",
|
|
249
|
+
transform: [{ translateY: Platform.OS === "web" ? -10 : -12 }],
|
|
250
|
+
zIndex: 1,
|
|
251
|
+
...(Platform.OS === "web" && { cursor: "pointer" }),
|
|
252
|
+
},
|
|
253
|
+
rightElements: {
|
|
254
|
+
position: "absolute",
|
|
255
|
+
right: spacing.sm,
|
|
256
|
+
top: 0,
|
|
257
|
+
bottom: 0,
|
|
258
|
+
flexDirection: "row",
|
|
259
|
+
alignItems: "center",
|
|
260
|
+
gap: spacing.xs,
|
|
261
|
+
zIndex: 1,
|
|
262
|
+
},
|
|
263
|
+
});
|