@timmbr/native-ui 0.0.0-stage → 1.0.1
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/dist/components/Actionsheet/Actionsheet.d.ts +12 -0
- package/dist/components/Actionsheet/Actionsheet.d.ts.map +1 -0
- package/dist/components/Actionsheet/Actionsheet.js +116 -0
- package/dist/components/Actionsheet/Actionsheet.styles.d.ts +43 -0
- package/dist/components/Actionsheet/Actionsheet.styles.d.ts.map +1 -0
- package/dist/components/Actionsheet/Actionsheet.styles.js +25 -0
- package/dist/components/Actionsheet/Actionsheet.types.d.ts +39 -0
- package/dist/components/Actionsheet/Actionsheet.types.d.ts.map +1 -0
- package/dist/components/Actionsheet/index.d.ts +4 -0
- package/dist/components/Actionsheet/index.d.ts.map +1 -0
- package/dist/components/Avatar/Avatar.d.ts +8 -0
- package/dist/components/Avatar/Avatar.d.ts.map +1 -0
- package/dist/components/Avatar/Avatar.js +66 -0
- package/dist/components/Avatar/Avatar.styles.d.ts +25 -0
- package/dist/components/Avatar/Avatar.styles.d.ts.map +1 -0
- package/dist/components/Avatar/Avatar.styles.js +29 -0
- package/dist/components/Avatar/Avatar.types.d.ts +21 -0
- package/dist/components/Avatar/Avatar.types.d.ts.map +1 -0
- package/dist/components/Avatar/index.d.ts +4 -0
- package/dist/components/Avatar/index.d.ts.map +1 -0
- package/dist/components/Badge/Badge.d.ts +7 -0
- package/dist/components/Badge/Badge.d.ts.map +1 -0
- package/dist/components/Badge/Badge.js +73 -0
- package/dist/components/Badge/Badge.styles.d.ts +15 -0
- package/dist/components/Badge/Badge.styles.d.ts.map +1 -0
- package/dist/components/Badge/Badge.styles.js +71 -0
- package/dist/components/Badge/Badge.types.d.ts +25 -0
- package/dist/components/Badge/Badge.types.d.ts.map +1 -0
- package/dist/components/Badge/index.d.ts +4 -0
- package/dist/components/Badge/index.d.ts.map +1 -0
- package/dist/components/Box/Box.d.ts +4 -0
- package/dist/components/Box/Box.d.ts.map +1 -0
- package/dist/components/Box/Box.js +26 -0
- package/dist/components/Box/Box.styles.d.ts +7 -0
- package/dist/components/Box/Box.styles.d.ts.map +1 -0
- package/dist/components/Box/Box.styles.js +7 -0
- package/dist/components/Box/Box.types.d.ts +8 -0
- package/dist/components/Box/Box.types.d.ts.map +1 -0
- package/dist/components/Box/index.d.ts +4 -0
- package/dist/components/Box/index.d.ts.map +1 -0
- package/dist/components/Button/Button.d.ts +9 -0
- package/dist/components/Button/Button.d.ts.map +1 -0
- package/dist/components/Button/Button.js +169 -0
- package/dist/components/Button/Button.styles.d.ts +30 -0
- package/dist/components/Button/Button.styles.d.ts.map +1 -0
- package/dist/components/Button/Button.styles.js +106 -0
- package/dist/components/Button/Button.types.d.ts +41 -0
- package/dist/components/Button/Button.types.d.ts.map +1 -0
- package/dist/components/Button/index.d.ts +4 -0
- package/dist/components/Button/index.d.ts.map +1 -0
- package/dist/components/Card/Card.d.ts +8 -0
- package/dist/components/Card/Card.d.ts.map +1 -0
- package/dist/components/Card/Card.js +50 -0
- package/dist/components/Card/Card.styles.d.ts +26 -0
- package/dist/components/Card/Card.styles.d.ts.map +1 -0
- package/dist/components/Card/Card.styles.js +32 -0
- package/dist/components/Card/Card.types.d.ts +26 -0
- package/dist/components/Card/Card.types.d.ts.map +1 -0
- package/dist/components/Card/index.d.ts +4 -0
- package/dist/components/Card/index.d.ts.map +1 -0
- package/dist/components/Divider/Divider.d.ts +4 -0
- package/dist/components/Divider/Divider.d.ts.map +1 -0
- package/dist/components/Divider/Divider.js +22 -0
- package/dist/components/Divider/Divider.styles.d.ts +7 -0
- package/dist/components/Divider/Divider.styles.d.ts.map +1 -0
- package/dist/components/Divider/Divider.styles.js +16 -0
- package/dist/components/Divider/Divider.types.d.ts +7 -0
- package/dist/components/Divider/Divider.types.d.ts.map +1 -0
- package/dist/components/Divider/index.d.ts +4 -0
- package/dist/components/Divider/index.d.ts.map +1 -0
- package/dist/components/HStack/HStack.d.ts +4 -0
- package/dist/components/HStack/HStack.d.ts.map +1 -0
- package/dist/components/HStack/HStack.js +21 -0
- package/dist/components/HStack/HStack.styles.d.ts +8 -0
- package/dist/components/HStack/HStack.styles.d.ts.map +1 -0
- package/dist/components/HStack/HStack.styles.js +28 -0
- package/dist/components/HStack/HStack.types.d.ts +9 -0
- package/dist/components/HStack/HStack.types.d.ts.map +1 -0
- package/dist/components/HStack/index.d.ts +4 -0
- package/dist/components/HStack/index.d.ts.map +1 -0
- package/dist/components/Heading/Heading.d.ts +4 -0
- package/dist/components/Heading/Heading.d.ts.map +1 -0
- package/dist/components/Heading/Heading.js +22 -0
- package/dist/components/Heading/Heading.styles.d.ts +8 -0
- package/dist/components/Heading/Heading.styles.d.ts.map +1 -0
- package/dist/components/Heading/Heading.styles.js +28 -0
- package/dist/components/Heading/Heading.types.d.ts +10 -0
- package/dist/components/Heading/Heading.types.d.ts.map +1 -0
- package/dist/components/Heading/index.d.ts +4 -0
- package/dist/components/Heading/index.d.ts.map +1 -0
- package/dist/components/Icon/Icon.d.ts +4 -0
- package/dist/components/Icon/Icon.d.ts.map +1 -0
- package/dist/components/Icon/Icon.js +37 -0
- package/dist/components/Icon/Icon.styles.d.ts +7 -0
- package/dist/components/Icon/Icon.styles.d.ts.map +1 -0
- package/dist/components/Icon/Icon.styles.js +20 -0
- package/dist/components/Icon/Icon.types.d.ts +9 -0
- package/dist/components/Icon/Icon.types.d.ts.map +1 -0
- package/dist/components/Icon/index.d.ts +4 -0
- package/dist/components/Icon/index.d.ts.map +1 -0
- package/dist/components/Input/Input.d.ts +8 -0
- package/dist/components/Input/Input.d.ts.map +1 -0
- package/dist/components/Input/Input.js +105 -0
- package/dist/components/Input/Input.styles.d.ts +23 -0
- package/dist/components/Input/Input.styles.d.ts.map +1 -0
- package/dist/components/Input/Input.styles.js +56 -0
- package/dist/components/Input/Input.types.d.ts +32 -0
- package/dist/components/Input/Input.types.d.ts.map +1 -0
- package/dist/components/Input/index.d.ts +4 -0
- package/dist/components/Input/index.d.ts.map +1 -0
- package/dist/components/Modal/Modal.d.ts +11 -0
- package/dist/components/Modal/Modal.d.ts.map +1 -0
- package/dist/components/Modal/Modal.js +102 -0
- package/dist/components/Modal/Modal.styles.d.ts +43 -0
- package/dist/components/Modal/Modal.styles.d.ts.map +1 -0
- package/dist/components/Modal/Modal.styles.js +37 -0
- package/dist/components/Modal/Modal.types.d.ts +38 -0
- package/dist/components/Modal/Modal.types.d.ts.map +1 -0
- package/dist/components/Modal/index.d.ts +4 -0
- package/dist/components/Modal/index.d.ts.map +1 -0
- package/dist/components/Pressable/Pressable.d.ts +4 -0
- package/dist/components/Pressable/Pressable.d.ts.map +1 -0
- package/dist/components/Pressable/Pressable.js +43 -0
- package/dist/components/Pressable/Pressable.styles.d.ts +7 -0
- package/dist/components/Pressable/Pressable.styles.d.ts.map +1 -0
- package/dist/components/Pressable/Pressable.styles.js +16 -0
- package/dist/components/Pressable/Pressable.types.d.ts +9 -0
- package/dist/components/Pressable/Pressable.types.d.ts.map +1 -0
- package/dist/components/Pressable/index.d.ts +4 -0
- package/dist/components/Pressable/index.d.ts.map +1 -0
- package/dist/components/Spinner/Spinner.d.ts +4 -0
- package/dist/components/Spinner/Spinner.d.ts.map +1 -0
- package/dist/components/Spinner/Spinner.js +52 -0
- package/dist/components/Spinner/Spinner.styles.d.ts +7 -0
- package/dist/components/Spinner/Spinner.styles.d.ts.map +1 -0
- package/dist/components/Spinner/Spinner.styles.js +20 -0
- package/dist/components/Spinner/Spinner.types.d.ts +8 -0
- package/dist/components/Spinner/Spinner.types.d.ts.map +1 -0
- package/dist/components/Spinner/index.d.ts +4 -0
- package/dist/components/Spinner/index.d.ts.map +1 -0
- package/dist/components/Switch/Switch.d.ts +4 -0
- package/dist/components/Switch/Switch.d.ts.map +1 -0
- package/dist/components/Switch/Switch.js +56 -0
- package/dist/components/Switch/Switch.styles.d.ts +16 -0
- package/dist/components/Switch/Switch.styles.d.ts.map +1 -0
- package/dist/components/Switch/Switch.styles.js +50 -0
- package/dist/components/Switch/Switch.types.d.ts +11 -0
- package/dist/components/Switch/Switch.types.d.ts.map +1 -0
- package/dist/components/Switch/index.d.ts +4 -0
- package/dist/components/Switch/index.d.ts.map +1 -0
- package/dist/components/Tabs/Tabs.d.ts +9 -0
- package/dist/components/Tabs/Tabs.d.ts.map +1 -0
- package/dist/components/Tabs/Tabs.js +109 -0
- package/dist/components/Tabs/Tabs.styles.d.ts +31 -0
- package/dist/components/Tabs/Tabs.styles.d.ts.map +1 -0
- package/dist/components/Tabs/Tabs.styles.js +37 -0
- package/dist/components/Tabs/Tabs.types.d.ts +31 -0
- package/dist/components/Tabs/Tabs.types.d.ts.map +1 -0
- package/dist/components/Tabs/index.d.ts +4 -0
- package/dist/components/Tabs/index.d.ts.map +1 -0
- package/dist/components/Text/Text.d.ts +4 -0
- package/dist/components/Text/Text.d.ts.map +1 -0
- package/dist/components/Text/Text.js +29 -0
- package/dist/components/Text/Text.styles.d.ts +9 -0
- package/dist/components/Text/Text.styles.d.ts.map +1 -0
- package/dist/components/Text/Text.styles.js +35 -0
- package/dist/components/Text/Text.types.d.ts +11 -0
- package/dist/components/Text/Text.types.d.ts.map +1 -0
- package/dist/components/Text/index.d.ts +4 -0
- package/dist/components/Text/index.d.ts.map +1 -0
- package/dist/components/Textarea/Textarea.d.ts +6 -0
- package/dist/components/Textarea/Textarea.d.ts.map +1 -0
- package/dist/components/Textarea/Textarea.js +66 -0
- package/dist/components/Textarea/Textarea.styles.d.ts +16 -0
- package/dist/components/Textarea/Textarea.styles.d.ts.map +1 -0
- package/dist/components/Textarea/Textarea.styles.js +47 -0
- package/dist/components/Textarea/Textarea.types.d.ts +20 -0
- package/dist/components/Textarea/Textarea.types.d.ts.map +1 -0
- package/dist/components/Textarea/index.d.ts +4 -0
- package/dist/components/Textarea/index.d.ts.map +1 -0
- package/dist/components/Toast/Toast.d.ts +7 -0
- package/dist/components/Toast/Toast.d.ts.map +1 -0
- package/dist/components/Toast/Toast.js +39 -0
- package/dist/components/Toast/Toast.styles.d.ts +20 -0
- package/dist/components/Toast/Toast.styles.d.ts.map +1 -0
- package/dist/components/Toast/Toast.styles.js +42 -0
- package/dist/components/Toast/Toast.types.d.ts +22 -0
- package/dist/components/Toast/Toast.types.d.ts.map +1 -0
- package/dist/components/Toast/index.d.ts +4 -0
- package/dist/components/Toast/index.d.ts.map +1 -0
- package/dist/components/VStack/VStack.d.ts +4 -0
- package/dist/components/VStack/VStack.d.ts.map +1 -0
- package/dist/components/VStack/VStack.js +21 -0
- package/dist/components/VStack/VStack.styles.d.ts +8 -0
- package/dist/components/VStack/VStack.styles.d.ts.map +1 -0
- package/dist/components/VStack/VStack.styles.js +28 -0
- package/dist/components/VStack/VStack.types.d.ts +9 -0
- package/dist/components/VStack/VStack.types.d.ts.map +1 -0
- package/dist/components/VStack/index.d.ts +4 -0
- package/dist/components/VStack/index.d.ts.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/provider/TimmbrNativeProvider.d.ts +21 -0
- package/dist/provider/TimmbrNativeProvider.d.ts.map +1 -0
- package/dist/provider/TimmbrNativeProvider.js +110 -0
- package/dist/theme/config.d.ts +360 -0
- package/dist/theme/config.d.ts.map +1 -0
- package/dist/theme/config.js +129 -0
- package/package.json +70 -3
- package/src/native/index.tsx +1019 -0
- package/README.md +0 -3
|
@@ -0,0 +1,1019 @@
|
|
|
1
|
+
import React, { createContext, useCallback, useContext, useState, useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text as RNText,
|
|
5
|
+
Pressable as RNPressable,
|
|
6
|
+
TextInput,
|
|
7
|
+
Modal as RNModal,
|
|
8
|
+
Switch as RNSwitch,
|
|
9
|
+
ActivityIndicator,
|
|
10
|
+
ViewProps,
|
|
11
|
+
TextProps,
|
|
12
|
+
PressableProps,
|
|
13
|
+
TextInputProps,
|
|
14
|
+
SwitchProps,
|
|
15
|
+
ActivityIndicatorProps,
|
|
16
|
+
ModalProps,
|
|
17
|
+
useColorScheme,
|
|
18
|
+
} from 'react-native';
|
|
19
|
+
|
|
20
|
+
export type ThemeMode = 'light' | 'dark' | 'system';
|
|
21
|
+
export type ResolvedTheme = 'light' | 'dark';
|
|
22
|
+
|
|
23
|
+
export interface TimmbrNativeContextValue {
|
|
24
|
+
mode: ThemeMode;
|
|
25
|
+
resolvedTheme: ResolvedTheme;
|
|
26
|
+
setMode: (mode: ThemeMode) => void;
|
|
27
|
+
toggleTheme: () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const TimmbrNativeContext = createContext<TimmbrNativeContextValue>({
|
|
31
|
+
mode: 'system',
|
|
32
|
+
resolvedTheme: 'light',
|
|
33
|
+
setMode: () => {},
|
|
34
|
+
toggleTheme: () => {},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const useTimmbrNative = (): TimmbrNativeContextValue => useContext(TimmbrNativeContext);
|
|
38
|
+
export const useTimmbrTheme = useTimmbrNative;
|
|
39
|
+
|
|
40
|
+
export interface TimmbrNativeProviderProps {
|
|
41
|
+
mode?: ThemeMode;
|
|
42
|
+
defaultMode?: ThemeMode;
|
|
43
|
+
children: React.ReactNode;
|
|
44
|
+
className?: string;
|
|
45
|
+
colorMode?: ThemeMode;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const TimmbrNativeProvider: React.FC<TimmbrNativeProviderProps> = ({
|
|
49
|
+
mode: controlledMode,
|
|
50
|
+
defaultMode = 'system',
|
|
51
|
+
colorMode,
|
|
52
|
+
children,
|
|
53
|
+
className = '',
|
|
54
|
+
}) => {
|
|
55
|
+
const systemScheme = useColorScheme();
|
|
56
|
+
const [uncontrolledMode, setUncontrolledMode] = useState<ThemeMode>(
|
|
57
|
+
colorMode ?? defaultMode
|
|
58
|
+
);
|
|
59
|
+
const currentMode = controlledMode ?? colorMode ?? uncontrolledMode;
|
|
60
|
+
|
|
61
|
+
const resolvedTheme: ResolvedTheme = useMemo(() => {
|
|
62
|
+
if (currentMode === 'system') {
|
|
63
|
+
return systemScheme === 'dark' ? 'dark' : 'light';
|
|
64
|
+
}
|
|
65
|
+
return currentMode;
|
|
66
|
+
}, [currentMode, systemScheme]);
|
|
67
|
+
|
|
68
|
+
const setMode = useCallback((newMode: ThemeMode) => {
|
|
69
|
+
setUncontrolledMode(newMode);
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
const toggleTheme = useCallback(() => {
|
|
73
|
+
setUncontrolledMode((prev) => {
|
|
74
|
+
const active = prev === 'system' ? (systemScheme === 'dark' ? 'dark' : 'light') : prev;
|
|
75
|
+
return active === 'dark' ? 'light' : 'dark';
|
|
76
|
+
});
|
|
77
|
+
}, [systemScheme]);
|
|
78
|
+
|
|
79
|
+
const contextValue = useMemo(
|
|
80
|
+
() => ({
|
|
81
|
+
mode: currentMode,
|
|
82
|
+
resolvedTheme,
|
|
83
|
+
setMode,
|
|
84
|
+
toggleTheme,
|
|
85
|
+
}),
|
|
86
|
+
[currentMode, resolvedTheme, setMode, toggleTheme]
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<TimmbrNativeContext.Provider value={contextValue}>
|
|
91
|
+
<View
|
|
92
|
+
className={`flex-1 ${resolvedTheme === 'dark' ? 'dark bg-background' : 'bg-background'} ${className}`}
|
|
93
|
+
>
|
|
94
|
+
{children}
|
|
95
|
+
</View>
|
|
96
|
+
</TimmbrNativeContext.Provider>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export interface BoxProps extends ViewProps {
|
|
101
|
+
className?: string;
|
|
102
|
+
}
|
|
103
|
+
export const Box: React.FC<BoxProps> = ({ className = '', style, children, ...props }) => (
|
|
104
|
+
<View className={className} style={style} {...props}>
|
|
105
|
+
{children}
|
|
106
|
+
</View>
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
export interface StackProps extends ViewProps {
|
|
110
|
+
className?: string;
|
|
111
|
+
space?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
112
|
+
reversed?: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const spaceGapMap: Record<string, string> = {
|
|
116
|
+
xs: 'gap-1',
|
|
117
|
+
sm: 'gap-2',
|
|
118
|
+
md: 'gap-3',
|
|
119
|
+
lg: 'gap-4',
|
|
120
|
+
xl: 'gap-6',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const VStack: React.FC<StackProps> = ({
|
|
124
|
+
space,
|
|
125
|
+
className = '',
|
|
126
|
+
reversed,
|
|
127
|
+
children,
|
|
128
|
+
...props
|
|
129
|
+
}) => {
|
|
130
|
+
const gapClass = space ? spaceGapMap[space] : '';
|
|
131
|
+
const dirClass = reversed ? 'flex-col-reverse' : 'flex-col';
|
|
132
|
+
return (
|
|
133
|
+
<View className={`${dirClass} ${gapClass} ${className}`} {...props}>
|
|
134
|
+
{children}
|
|
135
|
+
</View>
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const HStack: React.FC<StackProps> = ({
|
|
140
|
+
space,
|
|
141
|
+
className = '',
|
|
142
|
+
reversed,
|
|
143
|
+
children,
|
|
144
|
+
...props
|
|
145
|
+
}) => {
|
|
146
|
+
const gapClass = space ? spaceGapMap[space] : '';
|
|
147
|
+
const dirClass = reversed ? 'flex-row-reverse' : 'flex-row items-center';
|
|
148
|
+
return (
|
|
149
|
+
<View className={`${dirClass} ${gapClass} ${className}`} {...props}>
|
|
150
|
+
{children}
|
|
151
|
+
</View>
|
|
152
|
+
);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export interface TextPropsWithClass extends TextProps {
|
|
156
|
+
className?: string;
|
|
157
|
+
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const textSizeMap: Record<string, string> = {
|
|
161
|
+
xs: 'text-xs',
|
|
162
|
+
sm: 'text-sm',
|
|
163
|
+
md: 'text-base',
|
|
164
|
+
lg: 'text-lg',
|
|
165
|
+
xl: 'text-xl',
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export const Text: React.FC<TextPropsWithClass> = ({
|
|
169
|
+
size,
|
|
170
|
+
className = '',
|
|
171
|
+
children,
|
|
172
|
+
...props
|
|
173
|
+
}) => {
|
|
174
|
+
const sizeClass = size ? textSizeMap[size] : '';
|
|
175
|
+
return (
|
|
176
|
+
<RNText className={`text-foreground font-sans ${sizeClass} ${className}`} {...props}>
|
|
177
|
+
{children}
|
|
178
|
+
</RNText>
|
|
179
|
+
);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export interface HeadingProps extends TextProps {
|
|
183
|
+
className?: string;
|
|
184
|
+
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const headingSizeMap: Record<string, string> = {
|
|
188
|
+
sm: 'text-lg',
|
|
189
|
+
md: 'text-xl',
|
|
190
|
+
lg: 'text-2xl',
|
|
191
|
+
xl: 'text-3xl',
|
|
192
|
+
'2xl': 'text-4xl',
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export const Heading: React.FC<HeadingProps> = ({
|
|
196
|
+
size = 'lg' as NonNullable<HeadingProps['size']>,
|
|
197
|
+
className = '',
|
|
198
|
+
children,
|
|
199
|
+
...props
|
|
200
|
+
}) => {
|
|
201
|
+
return (
|
|
202
|
+
<RNText
|
|
203
|
+
className={`font-title font-bold text-foreground ${headingSizeMap[size]} ${className}`}
|
|
204
|
+
{...props}
|
|
205
|
+
>
|
|
206
|
+
{children}
|
|
207
|
+
</RNText>
|
|
208
|
+
);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const NON_COLOR_TEXT_UTILITY =
|
|
212
|
+
/^(?:dark:)?text-(xs|sm|base|lg|xl|2xl|3xl|4xl|left|center|right|justify|auto|\[\d+(px|rem|em)?\])$/;
|
|
213
|
+
|
|
214
|
+
function hasExplicitTextColorClass(className: string): boolean {
|
|
215
|
+
return className
|
|
216
|
+
.split(/\s+/)
|
|
217
|
+
.filter(Boolean)
|
|
218
|
+
.some((token) => /^(?:dark:)?text-/.test(token) && !NON_COLOR_TEXT_UTILITY.test(token));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export type ButtonVariant = 'solid' | 'outline' | 'link';
|
|
222
|
+
export type ButtonAction = 'primary' | 'secondary' | 'positive' | 'negative' | 'default';
|
|
223
|
+
export type ButtonSize = 'sm' | 'md' | 'lg';
|
|
224
|
+
|
|
225
|
+
interface ButtonStyleContextValue {
|
|
226
|
+
variant: ButtonVariant;
|
|
227
|
+
action: ButtonAction;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const ButtonStyleContext = createContext<ButtonStyleContextValue>({
|
|
231
|
+
variant: 'solid',
|
|
232
|
+
action: 'primary',
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
export interface ButtonProps extends Omit<PressableProps, 'children'> {
|
|
236
|
+
className?: string;
|
|
237
|
+
variant?: ButtonVariant;
|
|
238
|
+
action?: ButtonAction;
|
|
239
|
+
size?: ButtonSize;
|
|
240
|
+
isDisabled?: boolean;
|
|
241
|
+
isLoading?: boolean;
|
|
242
|
+
children?: React.ReactNode;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const buttonVariantStyles: Record<string, Record<string, string>> = {
|
|
246
|
+
solid: {
|
|
247
|
+
primary: 'bg-primary border-primary',
|
|
248
|
+
secondary: 'bg-secondary border-border',
|
|
249
|
+
positive: 'bg-success border-success',
|
|
250
|
+
negative: 'bg-destructive border-destructive',
|
|
251
|
+
default: 'bg-secondary border-border',
|
|
252
|
+
},
|
|
253
|
+
outline: {
|
|
254
|
+
primary: 'bg-transparent border-primary',
|
|
255
|
+
secondary: 'bg-transparent border-border',
|
|
256
|
+
positive: 'bg-transparent border-success',
|
|
257
|
+
negative: 'bg-transparent border-destructive',
|
|
258
|
+
default: 'bg-transparent border-border',
|
|
259
|
+
},
|
|
260
|
+
link: {
|
|
261
|
+
primary: 'bg-transparent border-transparent',
|
|
262
|
+
secondary: 'bg-transparent border-transparent',
|
|
263
|
+
positive: 'bg-transparent border-transparent',
|
|
264
|
+
negative: 'bg-transparent border-transparent',
|
|
265
|
+
default: 'bg-transparent border-transparent',
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const buttonTextColorStyles: Record<string, Record<string, string>> = {
|
|
270
|
+
solid: {
|
|
271
|
+
primary: 'text-primary-foreground',
|
|
272
|
+
secondary: 'text-secondary-foreground',
|
|
273
|
+
positive: 'text-primary-foreground',
|
|
274
|
+
negative: 'text-primary-foreground',
|
|
275
|
+
default: 'text-secondary-foreground',
|
|
276
|
+
},
|
|
277
|
+
outline: {
|
|
278
|
+
primary: 'text-primary',
|
|
279
|
+
secondary: 'text-foreground',
|
|
280
|
+
positive: 'text-emerald-600 dark:text-emerald-400',
|
|
281
|
+
negative: 'text-destructive',
|
|
282
|
+
default: 'text-foreground',
|
|
283
|
+
},
|
|
284
|
+
link: {
|
|
285
|
+
primary: 'text-primary',
|
|
286
|
+
secondary: 'text-foreground',
|
|
287
|
+
positive: 'text-emerald-600 dark:text-emerald-400',
|
|
288
|
+
negative: 'text-destructive',
|
|
289
|
+
default: 'text-primary',
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
function resolveButtonSpinnerColor(
|
|
294
|
+
variant: ButtonVariant,
|
|
295
|
+
action: ButtonAction,
|
|
296
|
+
resolvedTheme: ResolvedTheme
|
|
297
|
+
): string {
|
|
298
|
+
if (variant === 'solid' && action !== 'secondary' && action !== 'default') {
|
|
299
|
+
return '#FFFFFF';
|
|
300
|
+
}
|
|
301
|
+
if (variant === 'solid') {
|
|
302
|
+
return resolvedTheme === 'dark' ? '#F5EDE0' : '#60321D';
|
|
303
|
+
}
|
|
304
|
+
if (action === 'primary') return '#C0643A';
|
|
305
|
+
if (action === 'positive') return '#059669';
|
|
306
|
+
if (action === 'negative') return resolvedTheme === 'dark' ? '#EF4444' : '#DC2626';
|
|
307
|
+
return resolvedTheme === 'dark' ? '#F7F1E6' : '#0B0B0B';
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const buttonSizeStyles: Record<string, string> = {
|
|
311
|
+
sm: 'px-3 py-1.5 rounded-md min-h-[36px]',
|
|
312
|
+
md: 'px-4 py-2.5 rounded-lg min-h-[44px]',
|
|
313
|
+
lg: 'px-6 py-3.5 rounded-xl min-h-[52px]',
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
export const Button: React.FC<ButtonProps> = ({
|
|
317
|
+
variant = 'solid' as ButtonVariant,
|
|
318
|
+
action = 'primary' as ButtonAction,
|
|
319
|
+
size = 'md' as ButtonSize,
|
|
320
|
+
isDisabled = false,
|
|
321
|
+
isLoading = false,
|
|
322
|
+
disabled,
|
|
323
|
+
className = '',
|
|
324
|
+
children,
|
|
325
|
+
...props
|
|
326
|
+
}) => {
|
|
327
|
+
const { resolvedTheme } = useTimmbrNative();
|
|
328
|
+
const isActuallyDisabled = isDisabled || isLoading || disabled;
|
|
329
|
+
const variantClass = buttonVariantStyles[variant]?.[action] || buttonVariantStyles.solid.primary;
|
|
330
|
+
const sizeClass = buttonSizeStyles[size] || buttonSizeStyles.md;
|
|
331
|
+
const disabledClass = isActuallyDisabled ? 'opacity-50' : 'active:opacity-85';
|
|
332
|
+
const spinnerColor = resolveButtonSpinnerColor(variant, action, resolvedTheme);
|
|
333
|
+
const styleValue = useMemo(() => ({ variant, action }), [variant, action]);
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
<ButtonStyleContext.Provider value={styleValue}>
|
|
337
|
+
<RNPressable
|
|
338
|
+
disabled={isActuallyDisabled}
|
|
339
|
+
className={`flex-row items-center justify-center border ${variantClass} ${sizeClass} ${disabledClass} ${className}`}
|
|
340
|
+
{...props}
|
|
341
|
+
>
|
|
342
|
+
{isLoading && <ActivityIndicator color={spinnerColor} className="mr-2" size="small" />}
|
|
343
|
+
{children}
|
|
344
|
+
</RNPressable>
|
|
345
|
+
</ButtonStyleContext.Provider>
|
|
346
|
+
);
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
export interface ButtonTextProps extends TextPropsWithClass {
|
|
350
|
+
action?: 'primary' | 'secondary' | 'positive' | 'negative' | 'default';
|
|
351
|
+
variant?: 'solid' | 'outline' | 'link';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export const ButtonText: React.FC<ButtonTextProps> = ({
|
|
355
|
+
variant: variantProp,
|
|
356
|
+
action: actionProp,
|
|
357
|
+
className = '',
|
|
358
|
+
children,
|
|
359
|
+
...props
|
|
360
|
+
}) => {
|
|
361
|
+
const parentStyle = useContext(ButtonStyleContext);
|
|
362
|
+
const variant = variantProp ?? parentStyle.variant;
|
|
363
|
+
const action = actionProp ?? parentStyle.action;
|
|
364
|
+
const defaultColorClass = hasExplicitTextColorClass(className)
|
|
365
|
+
? ''
|
|
366
|
+
: buttonTextColorStyles[variant]?.[action] || 'text-primary-foreground';
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<RNText
|
|
370
|
+
className={`text-center font-semibold font-sans text-sm ${defaultColorClass} ${className}`}
|
|
371
|
+
{...props}
|
|
372
|
+
>
|
|
373
|
+
{children}
|
|
374
|
+
</RNText>
|
|
375
|
+
);
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
export interface ButtonSpinnerProps extends ActivityIndicatorProps {
|
|
379
|
+
className?: string;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export const ButtonSpinner: React.FC<ButtonSpinnerProps> = ({
|
|
383
|
+
color,
|
|
384
|
+
size = 'small',
|
|
385
|
+
className = 'mr-2',
|
|
386
|
+
...props
|
|
387
|
+
}) => {
|
|
388
|
+
const { resolvedTheme } = useTimmbrNative();
|
|
389
|
+
const parentStyle = useContext(ButtonStyleContext);
|
|
390
|
+
const spinnerColor =
|
|
391
|
+
color ?? resolveButtonSpinnerColor(parentStyle.variant, parentStyle.action, resolvedTheme);
|
|
392
|
+
|
|
393
|
+
return <ActivityIndicator color={spinnerColor} size={size} className={className} {...props} />;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
export type BadgeVariant = 'solid' | 'outline';
|
|
397
|
+
export type BadgeAction = 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | 'muted';
|
|
398
|
+
|
|
399
|
+
interface BadgeStyleContextValue {
|
|
400
|
+
variant: BadgeVariant;
|
|
401
|
+
action: BadgeAction;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const BadgeStyleContext = createContext<BadgeStyleContextValue>({
|
|
405
|
+
variant: 'solid',
|
|
406
|
+
action: 'primary',
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
export interface BadgeProps extends ViewProps {
|
|
410
|
+
className?: string;
|
|
411
|
+
variant?: BadgeVariant;
|
|
412
|
+
action?: BadgeAction;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const badgeActionStyles: Record<string, Record<string, string>> = {
|
|
416
|
+
solid: {
|
|
417
|
+
primary: 'bg-primary border-primary',
|
|
418
|
+
secondary: 'bg-secondary border-border',
|
|
419
|
+
success: 'bg-emerald-600 border-emerald-600',
|
|
420
|
+
warning: 'bg-amber-600 border-amber-600',
|
|
421
|
+
error: 'bg-destructive border-destructive',
|
|
422
|
+
info: 'bg-blue-600 border-blue-600',
|
|
423
|
+
muted: 'bg-muted border-border',
|
|
424
|
+
},
|
|
425
|
+
outline: {
|
|
426
|
+
primary: 'bg-transparent border-primary',
|
|
427
|
+
secondary: 'bg-transparent border-border',
|
|
428
|
+
success: 'bg-transparent border-emerald-600',
|
|
429
|
+
warning: 'bg-transparent border-amber-600',
|
|
430
|
+
error: 'bg-transparent border-destructive',
|
|
431
|
+
info: 'bg-transparent border-blue-600',
|
|
432
|
+
muted: 'bg-transparent border-border',
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const badgeTextColorStyles: Record<string, Record<string, string>> = {
|
|
437
|
+
solid: {
|
|
438
|
+
primary: 'text-primary-foreground',
|
|
439
|
+
secondary: 'text-secondary-foreground',
|
|
440
|
+
success: 'text-primary-foreground',
|
|
441
|
+
warning: 'text-primary-foreground',
|
|
442
|
+
error: 'text-primary-foreground',
|
|
443
|
+
info: 'text-primary-foreground',
|
|
444
|
+
muted: 'text-foreground',
|
|
445
|
+
},
|
|
446
|
+
outline: {
|
|
447
|
+
primary: 'text-primary',
|
|
448
|
+
secondary: 'text-foreground',
|
|
449
|
+
success: 'text-emerald-600 dark:text-emerald-400',
|
|
450
|
+
warning: 'text-amber-600 dark:text-amber-400',
|
|
451
|
+
error: 'text-destructive',
|
|
452
|
+
info: 'text-blue-600 dark:text-blue-400',
|
|
453
|
+
muted: 'text-muted-foreground',
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
export const Badge: React.FC<BadgeProps> = ({
|
|
458
|
+
variant = 'solid' as BadgeVariant,
|
|
459
|
+
action = 'primary' as BadgeAction,
|
|
460
|
+
className = '',
|
|
461
|
+
children,
|
|
462
|
+
...props
|
|
463
|
+
}) => {
|
|
464
|
+
const badgeClass = badgeActionStyles[variant]?.[action] || badgeActionStyles.solid.primary;
|
|
465
|
+
const styleValue = useMemo(() => ({ variant, action }), [variant, action]);
|
|
466
|
+
|
|
467
|
+
return (
|
|
468
|
+
<BadgeStyleContext.Provider value={styleValue}>
|
|
469
|
+
<View
|
|
470
|
+
className={`px-2.5 py-0.5 rounded-full border items-center justify-center flex-row ${badgeClass} ${className}`}
|
|
471
|
+
{...props}
|
|
472
|
+
>
|
|
473
|
+
{children}
|
|
474
|
+
</View>
|
|
475
|
+
</BadgeStyleContext.Provider>
|
|
476
|
+
);
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
export const BadgeText: React.FC<TextPropsWithClass> = ({
|
|
480
|
+
className = '',
|
|
481
|
+
children,
|
|
482
|
+
...props
|
|
483
|
+
}) => {
|
|
484
|
+
const parentStyle = useContext(BadgeStyleContext);
|
|
485
|
+
const defaultColorClass = hasExplicitTextColorClass(className)
|
|
486
|
+
? ''
|
|
487
|
+
: badgeTextColorStyles[parentStyle.variant]?.[parentStyle.action] || 'text-primary-foreground';
|
|
488
|
+
|
|
489
|
+
return (
|
|
490
|
+
<RNText
|
|
491
|
+
className={`text-xs font-semibold font-sans ${defaultColorClass} ${className}`}
|
|
492
|
+
{...props}
|
|
493
|
+
>
|
|
494
|
+
{children}
|
|
495
|
+
</RNText>
|
|
496
|
+
);
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
export const Card: React.FC<BoxProps> = ({ className = '', children, ...props }) => (
|
|
500
|
+
<View
|
|
501
|
+
className={`rounded-xl p-4 bg-card border border-border shadow-sm ${className}`}
|
|
502
|
+
{...props}
|
|
503
|
+
>
|
|
504
|
+
{children}
|
|
505
|
+
</View>
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
export interface InputProps extends ViewProps {
|
|
509
|
+
className?: string;
|
|
510
|
+
isInvalid?: boolean;
|
|
511
|
+
isDisabled?: boolean;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export const Input: React.FC<InputProps> = ({
|
|
515
|
+
isInvalid = false,
|
|
516
|
+
isDisabled = false,
|
|
517
|
+
className = '',
|
|
518
|
+
children,
|
|
519
|
+
...props
|
|
520
|
+
}) => {
|
|
521
|
+
const stateClass = isInvalid
|
|
522
|
+
? 'border-destructive'
|
|
523
|
+
: isDisabled
|
|
524
|
+
? 'border-border opacity-50'
|
|
525
|
+
: 'border-border';
|
|
526
|
+
return (
|
|
527
|
+
<View
|
|
528
|
+
className={`flex-row items-center rounded-lg border bg-background px-3 py-2 min-h-[44px] ${stateClass} ${className}`}
|
|
529
|
+
{...props}
|
|
530
|
+
>
|
|
531
|
+
{children}
|
|
532
|
+
</View>
|
|
533
|
+
);
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
export interface InputFieldProps extends TextInputProps {
|
|
537
|
+
className?: string;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export const InputField: React.FC<InputFieldProps> = ({
|
|
541
|
+
className = '',
|
|
542
|
+
placeholderTextColor,
|
|
543
|
+
...props
|
|
544
|
+
}) => {
|
|
545
|
+
const { resolvedTheme } = useTimmbrNative();
|
|
546
|
+
const defaultPlaceholderColor = resolvedTheme === 'dark' ? '#9D9D9D' : '#6D6D6D';
|
|
547
|
+
return (
|
|
548
|
+
<TextInput
|
|
549
|
+
className={`flex-1 text-foreground placeholder:text-muted-foreground font-sans text-base py-1 ${className}`}
|
|
550
|
+
placeholderTextColor={placeholderTextColor ?? defaultPlaceholderColor}
|
|
551
|
+
{...props}
|
|
552
|
+
/>
|
|
553
|
+
);
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
export const Textarea: React.FC<InputProps> = ({
|
|
557
|
+
isInvalid = false,
|
|
558
|
+
isDisabled = false,
|
|
559
|
+
className = '',
|
|
560
|
+
children,
|
|
561
|
+
...props
|
|
562
|
+
}) => {
|
|
563
|
+
const stateClass = isInvalid
|
|
564
|
+
? 'border-destructive'
|
|
565
|
+
: isDisabled
|
|
566
|
+
? 'border-border opacity-50'
|
|
567
|
+
: 'border-border';
|
|
568
|
+
return (
|
|
569
|
+
<View
|
|
570
|
+
className={`rounded-lg border bg-background p-3 min-h-[100px] ${stateClass} ${className}`}
|
|
571
|
+
{...props}
|
|
572
|
+
>
|
|
573
|
+
{children}
|
|
574
|
+
</View>
|
|
575
|
+
);
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
export const TextareaInput: React.FC<InputFieldProps> = ({
|
|
579
|
+
className = '',
|
|
580
|
+
placeholderTextColor,
|
|
581
|
+
...props
|
|
582
|
+
}) => {
|
|
583
|
+
const { resolvedTheme } = useTimmbrNative();
|
|
584
|
+
const defaultPlaceholderColor = resolvedTheme === 'dark' ? '#9D9D9D' : '#6D6D6D';
|
|
585
|
+
return (
|
|
586
|
+
<TextInput
|
|
587
|
+
multiline
|
|
588
|
+
textAlignVertical="top"
|
|
589
|
+
className={`flex-1 text-foreground placeholder:text-muted-foreground font-sans text-base ${className}`}
|
|
590
|
+
placeholderTextColor={placeholderTextColor ?? defaultPlaceholderColor}
|
|
591
|
+
{...props}
|
|
592
|
+
/>
|
|
593
|
+
);
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
interface TabsContextValue {
|
|
597
|
+
activeTab: string;
|
|
598
|
+
setActiveTab: (tab: string) => void;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const TabsContext = createContext<TabsContextValue>({
|
|
602
|
+
activeTab: '',
|
|
603
|
+
setActiveTab: () => {},
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
export interface TabsProps extends ViewProps {
|
|
607
|
+
defaultValue?: string;
|
|
608
|
+
value?: string;
|
|
609
|
+
onValueChange?: (value: string) => void;
|
|
610
|
+
className?: string;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export const Tabs: React.FC<TabsProps> = ({
|
|
614
|
+
defaultValue = '',
|
|
615
|
+
value: controlledValue,
|
|
616
|
+
onValueChange,
|
|
617
|
+
className = '',
|
|
618
|
+
children,
|
|
619
|
+
...props
|
|
620
|
+
}) => {
|
|
621
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
|
|
622
|
+
const activeTab = controlledValue ?? uncontrolledValue;
|
|
623
|
+
|
|
624
|
+
const setActiveTab = (newTab: string) => {
|
|
625
|
+
setUncontrolledValue(newTab);
|
|
626
|
+
onValueChange?.(newTab);
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
return (
|
|
630
|
+
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
|
|
631
|
+
<View className={`w-full ${className}`} {...props}>
|
|
632
|
+
{children}
|
|
633
|
+
</View>
|
|
634
|
+
</TabsContext.Provider>
|
|
635
|
+
);
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
export const TabsList: React.FC<BoxProps> = ({ className = '', children, ...props }) => (
|
|
639
|
+
<View
|
|
640
|
+
className={`flex-row rounded-lg bg-secondary p-1 border border-border ${className}`}
|
|
641
|
+
{...props}
|
|
642
|
+
>
|
|
643
|
+
{children}
|
|
644
|
+
</View>
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
export interface TabsTriggerProps extends PressableProps {
|
|
648
|
+
value: string;
|
|
649
|
+
className?: string;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
export const TabsTrigger: React.FC<TabsTriggerProps> = ({
|
|
653
|
+
value,
|
|
654
|
+
className = '',
|
|
655
|
+
children,
|
|
656
|
+
...props
|
|
657
|
+
}) => {
|
|
658
|
+
const { activeTab, setActiveTab } = useContext(TabsContext);
|
|
659
|
+
const isActive = activeTab === value;
|
|
660
|
+
|
|
661
|
+
return (
|
|
662
|
+
<RNPressable
|
|
663
|
+
onPress={() => setActiveTab(value)}
|
|
664
|
+
className={`flex-1 py-2 px-3 rounded-md items-center justify-center ${
|
|
665
|
+
isActive ? 'bg-card shadow-sm border border-border' : 'opacity-70'
|
|
666
|
+
} ${className}`}
|
|
667
|
+
{...props}
|
|
668
|
+
>
|
|
669
|
+
{children}
|
|
670
|
+
</RNPressable>
|
|
671
|
+
);
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
export const TabsTriggerText: React.FC<TextPropsWithClass> = ({
|
|
675
|
+
className = '',
|
|
676
|
+
children,
|
|
677
|
+
...props
|
|
678
|
+
}) => (
|
|
679
|
+
<RNText
|
|
680
|
+
className={`text-sm font-semibold text-foreground font-sans ${className}`}
|
|
681
|
+
{...props}
|
|
682
|
+
>
|
|
683
|
+
{children}
|
|
684
|
+
</RNText>
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
export interface TabsContentProps extends ViewProps {
|
|
688
|
+
value: string;
|
|
689
|
+
className?: string;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export const TabsContent: React.FC<TabsContentProps> = ({
|
|
693
|
+
value,
|
|
694
|
+
className = '',
|
|
695
|
+
children,
|
|
696
|
+
...props
|
|
697
|
+
}) => {
|
|
698
|
+
const { activeTab } = useContext(TabsContext);
|
|
699
|
+
if (activeTab !== value) return null;
|
|
700
|
+
|
|
701
|
+
return (
|
|
702
|
+
<View className={`mt-3 ${className}`} {...props}>
|
|
703
|
+
{children}
|
|
704
|
+
</View>
|
|
705
|
+
);
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
export interface ActionsheetProps {
|
|
709
|
+
isOpen: boolean;
|
|
710
|
+
onClose: () => void;
|
|
711
|
+
children: React.ReactNode;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export const Actionsheet: React.FC<ActionsheetProps> = ({ isOpen, onClose, children }) => {
|
|
715
|
+
const { resolvedTheme } = useTimmbrNative();
|
|
716
|
+
return (
|
|
717
|
+
<RNModal
|
|
718
|
+
visible={isOpen}
|
|
719
|
+
transparent
|
|
720
|
+
animationType="fade"
|
|
721
|
+
onRequestClose={onClose}
|
|
722
|
+
>
|
|
723
|
+
<View className={`flex-1 justify-end ${resolvedTheme === 'dark' ? 'dark' : ''}`}>
|
|
724
|
+
{children}
|
|
725
|
+
</View>
|
|
726
|
+
</RNModal>
|
|
727
|
+
);
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
export const ActionsheetBackdrop: React.FC<PressableProps & { className?: string }> = ({
|
|
731
|
+
onPress,
|
|
732
|
+
className = '',
|
|
733
|
+
}) => (
|
|
734
|
+
<RNPressable
|
|
735
|
+
onPress={onPress}
|
|
736
|
+
className={`absolute inset-0 bg-black/60 ${className}`}
|
|
737
|
+
/>
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
export const ActionsheetContent: React.FC<BoxProps> = ({
|
|
741
|
+
className = '',
|
|
742
|
+
children,
|
|
743
|
+
...props
|
|
744
|
+
}) => (
|
|
745
|
+
<View
|
|
746
|
+
className={`bg-card rounded-t-3xl p-5 border-t border-border shadow-lg max-h-[85%] ${className}`}
|
|
747
|
+
{...props}
|
|
748
|
+
>
|
|
749
|
+
{children}
|
|
750
|
+
</View>
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
export const ActionsheetDragIndicator: React.FC<BoxProps> = ({
|
|
754
|
+
className = '',
|
|
755
|
+
...props
|
|
756
|
+
}) => (
|
|
757
|
+
<View className="items-center pb-3">
|
|
758
|
+
<View className={`w-12 h-1.5 rounded-full bg-muted ${className}`} {...props} />
|
|
759
|
+
</View>
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
export interface ActionsheetItemProps extends PressableProps {
|
|
763
|
+
className?: string;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
export const ActionsheetItem: React.FC<ActionsheetItemProps> = ({
|
|
767
|
+
className = '',
|
|
768
|
+
children,
|
|
769
|
+
...props
|
|
770
|
+
}) => (
|
|
771
|
+
<RNPressable
|
|
772
|
+
className={`py-3.5 px-4 rounded-xl active:bg-secondary flex-row items-center ${className}`}
|
|
773
|
+
{...props}
|
|
774
|
+
>
|
|
775
|
+
{children}
|
|
776
|
+
</RNPressable>
|
|
777
|
+
);
|
|
778
|
+
|
|
779
|
+
export const ActionsheetItemText: React.FC<TextPropsWithClass> = ({
|
|
780
|
+
className = '',
|
|
781
|
+
children,
|
|
782
|
+
...props
|
|
783
|
+
}) => (
|
|
784
|
+
<RNText className={`text-base font-sans text-foreground ${className}`} {...props}>
|
|
785
|
+
{children}
|
|
786
|
+
</RNText>
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
export interface CustomModalProps extends ModalProps {
|
|
790
|
+
isOpen?: boolean;
|
|
791
|
+
onClose?: () => void;
|
|
792
|
+
className?: string;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
export const Modal: React.FC<CustomModalProps> = ({
|
|
796
|
+
isOpen = false,
|
|
797
|
+
visible,
|
|
798
|
+
onClose,
|
|
799
|
+
className = '',
|
|
800
|
+
children,
|
|
801
|
+
...props
|
|
802
|
+
}) => {
|
|
803
|
+
const { resolvedTheme } = useTimmbrNative();
|
|
804
|
+
return (
|
|
805
|
+
<RNModal
|
|
806
|
+
visible={isOpen || visible}
|
|
807
|
+
transparent
|
|
808
|
+
animationType="fade"
|
|
809
|
+
onRequestClose={onClose}
|
|
810
|
+
{...props}
|
|
811
|
+
>
|
|
812
|
+
<View
|
|
813
|
+
className={`flex-1 items-center justify-center bg-black/60 p-4 ${
|
|
814
|
+
resolvedTheme === 'dark' ? 'dark' : ''
|
|
815
|
+
} ${className}`}
|
|
816
|
+
>
|
|
817
|
+
{children}
|
|
818
|
+
</View>
|
|
819
|
+
</RNModal>
|
|
820
|
+
);
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
export interface CustomSwitchProps extends SwitchProps {
|
|
824
|
+
className?: string;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
export const Switch: React.FC<CustomSwitchProps> = ({
|
|
828
|
+
trackColor,
|
|
829
|
+
thumbColor = '#FFFFFF',
|
|
830
|
+
className = '',
|
|
831
|
+
...props
|
|
832
|
+
}) => {
|
|
833
|
+
const { resolvedTheme } = useTimmbrNative();
|
|
834
|
+
const defaultTrackColor = {
|
|
835
|
+
false: resolvedTheme === 'dark' ? '#3C3C3C' : '#DDD5CA',
|
|
836
|
+
true: '#C0643A',
|
|
837
|
+
};
|
|
838
|
+
return (
|
|
839
|
+
<RNSwitch
|
|
840
|
+
trackColor={trackColor ?? defaultTrackColor}
|
|
841
|
+
thumbColor={thumbColor}
|
|
842
|
+
className={className}
|
|
843
|
+
{...props}
|
|
844
|
+
/>
|
|
845
|
+
);
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
export const Spinner: React.FC<ButtonSpinnerProps> = ({
|
|
849
|
+
color = '#C0643A',
|
|
850
|
+
size = 'small',
|
|
851
|
+
className = '',
|
|
852
|
+
...props
|
|
853
|
+
}) => <ActivityIndicator color={color} size={size} className={className} {...props} />;
|
|
854
|
+
|
|
855
|
+
export interface AvatarProps extends ViewProps {
|
|
856
|
+
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
857
|
+
className?: string;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const avatarSizeMap: Record<string, string> = {
|
|
861
|
+
sm: 'w-8 h-8 rounded-full',
|
|
862
|
+
md: 'w-10 h-10 rounded-full',
|
|
863
|
+
lg: 'w-14 h-14 rounded-full',
|
|
864
|
+
xl: 'w-20 h-20 rounded-full',
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
export const Avatar: React.FC<AvatarProps> = ({
|
|
868
|
+
size = 'md',
|
|
869
|
+
className = '',
|
|
870
|
+
children,
|
|
871
|
+
...props
|
|
872
|
+
}) => (
|
|
873
|
+
<View
|
|
874
|
+
className={`items-center justify-center bg-secondary border border-border overflow-hidden ${avatarSizeMap[size]} ${className}`}
|
|
875
|
+
{...props}
|
|
876
|
+
>
|
|
877
|
+
{children}
|
|
878
|
+
</View>
|
|
879
|
+
);
|
|
880
|
+
|
|
881
|
+
export const AvatarFallbackText: React.FC<TextPropsWithClass> = ({
|
|
882
|
+
className = '',
|
|
883
|
+
children,
|
|
884
|
+
...props
|
|
885
|
+
}) => (
|
|
886
|
+
<RNText
|
|
887
|
+
className={`font-bold font-sans text-secondary-foreground text-sm ${className}`}
|
|
888
|
+
{...props}
|
|
889
|
+
>
|
|
890
|
+
{children}
|
|
891
|
+
</RNText>
|
|
892
|
+
);
|
|
893
|
+
|
|
894
|
+
export const Divider: React.FC<BoxProps> = ({ className = '', ...props }) => (
|
|
895
|
+
<View className={`h-px bg-border my-2 w-full ${className}`} {...props} />
|
|
896
|
+
);
|
|
897
|
+
|
|
898
|
+
export const Pressable: React.FC<PressableProps & { className?: string }> = ({
|
|
899
|
+
className = '',
|
|
900
|
+
children,
|
|
901
|
+
...props
|
|
902
|
+
}) => (
|
|
903
|
+
<RNPressable className={className} {...props}>
|
|
904
|
+
{children}
|
|
905
|
+
</RNPressable>
|
|
906
|
+
);
|
|
907
|
+
|
|
908
|
+
export interface ToastProps extends ViewProps {
|
|
909
|
+
action?: 'success' | 'warning' | 'error' | 'info';
|
|
910
|
+
title?: string;
|
|
911
|
+
description?: string;
|
|
912
|
+
className?: string;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
export const Toast: React.FC<ToastProps> = ({
|
|
916
|
+
action = 'info',
|
|
917
|
+
title,
|
|
918
|
+
description,
|
|
919
|
+
className = '',
|
|
920
|
+
children,
|
|
921
|
+
...props
|
|
922
|
+
}) => {
|
|
923
|
+
const borderColors: Record<string, string> = {
|
|
924
|
+
success: 'border-emerald-600',
|
|
925
|
+
warning: 'border-amber-600',
|
|
926
|
+
error: 'border-destructive',
|
|
927
|
+
info: 'border-blue-600',
|
|
928
|
+
};
|
|
929
|
+
return (
|
|
930
|
+
<View
|
|
931
|
+
className={`p-4 rounded-xl bg-card border-l-4 shadow-md ${borderColors[action]} ${className}`}
|
|
932
|
+
{...props}
|
|
933
|
+
>
|
|
934
|
+
{title && <RNText className="font-bold text-foreground">{title}</RNText>}
|
|
935
|
+
{description && <RNText className="text-sm text-muted-foreground mt-1">{description}</RNText>}
|
|
936
|
+
{children}
|
|
937
|
+
</View>
|
|
938
|
+
);
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
export interface IconProps {
|
|
942
|
+
as?: React.ElementType;
|
|
943
|
+
size?: number | string;
|
|
944
|
+
color?: string;
|
|
945
|
+
className?: string;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function getSemanticIconColorMap(resolvedTheme: ResolvedTheme): Record<string, string> {
|
|
949
|
+
const isDark = resolvedTheme === 'dark';
|
|
950
|
+
return {
|
|
951
|
+
'text-foreground': isDark ? '#F7F1E6' : '#0B0B0B',
|
|
952
|
+
'text-card-foreground': isDark ? '#F7F1E6' : '#0B0B0B',
|
|
953
|
+
'text-muted-foreground': isDark ? '#9D9D9D' : '#6D6D6D',
|
|
954
|
+
'text-secondary-foreground': isDark ? '#F5EDE0' : '#60321D',
|
|
955
|
+
'text-accent-foreground': isDark ? '#F5EDE0' : '#60321D',
|
|
956
|
+
'text-primary': '#C0643A',
|
|
957
|
+
'text-primary-foreground': '#FFFFFF',
|
|
958
|
+
'text-white': '#FFFFFF',
|
|
959
|
+
'text-destructive': isDark ? '#EF4444' : '#DC2626',
|
|
960
|
+
'text-destructive-foreground': isDark ? '#F7F1E6' : '#FFFFFF',
|
|
961
|
+
'text-success': '#059669',
|
|
962
|
+
'text-emerald-500': '#10B981',
|
|
963
|
+
'text-emerald-600': '#059669',
|
|
964
|
+
'text-warning': '#D97706',
|
|
965
|
+
'text-amber-400': '#FBBF24',
|
|
966
|
+
'text-amber-500': '#F59E0B',
|
|
967
|
+
'text-amber-600': '#D97706',
|
|
968
|
+
'text-info': '#2563EB',
|
|
969
|
+
'text-blue-500': '#3B82F6',
|
|
970
|
+
'text-blue-600': '#2563EB',
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function resolveIconColor(
|
|
975
|
+
explicitColor: string | undefined,
|
|
976
|
+
className: string,
|
|
977
|
+
resolvedTheme: ResolvedTheme
|
|
978
|
+
): string {
|
|
979
|
+
if (explicitColor && explicitColor !== 'currentColor') {
|
|
980
|
+
return explicitColor;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const tokens = className.split(/\s+/).filter(Boolean);
|
|
984
|
+
const colorMap = getSemanticIconColorMap(resolvedTheme);
|
|
985
|
+
|
|
986
|
+
if (resolvedTheme === 'dark') {
|
|
987
|
+
for (const token of tokens) {
|
|
988
|
+
if (token.startsWith('dark:')) {
|
|
989
|
+
const unprefixed = token.slice('dark:'.length);
|
|
990
|
+
const hexMatch = /^text-\[(#[0-9a-fA-F]{3,8})\]$/.exec(unprefixed);
|
|
991
|
+
if (hexMatch?.[1]) return hexMatch[1];
|
|
992
|
+
if (colorMap[unprefixed]) return colorMap[unprefixed];
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
for (const token of tokens) {
|
|
998
|
+
if (token.startsWith('dark:')) continue;
|
|
999
|
+
const hexMatch = /^text-\[(#[0-9a-fA-F]{3,8})\]$/.exec(token);
|
|
1000
|
+
if (hexMatch?.[1]) return hexMatch[1];
|
|
1001
|
+
if (colorMap[token]) return colorMap[token];
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
return resolvedTheme === 'dark' ? '#F7F1E6' : '#0B0B0B';
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
export const Icon: React.FC<IconProps> = ({
|
|
1008
|
+
as: Component,
|
|
1009
|
+
size = 20,
|
|
1010
|
+
color,
|
|
1011
|
+
className = '',
|
|
1012
|
+
}) => {
|
|
1013
|
+
const { resolvedTheme } = useTimmbrNative();
|
|
1014
|
+
if (!Component) return null;
|
|
1015
|
+
const numSize = typeof size === 'number' ? size : parseInt(size, 10) || 20;
|
|
1016
|
+
const resolvedColor = resolveIconColor(color, className, resolvedTheme);
|
|
1017
|
+
return <Component size={numSize} color={resolvedColor} className={className} />;
|
|
1018
|
+
};
|
|
1019
|
+
|